2

I'm trying this Go code

package main

import (
    "github.com/gorilla/mux"
    "io"
    "log"
    "net/http"
)

func HealthCheckHandler(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Header().Set("Content-Type", "application/json")

    io.WriteString(w, `{"alive": true}`)
}

func main() {

    router := mux.NewRouter()
    router.HandleFunc("/health", HealthCheckHandler).Methods("GET")

    log.Printf("running server ...")
    log.Fatal(http.ListenAndServe(":8000", router))
}

with this test

package main

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestHealthCheckHandler(t *testing.T) {
    req, err := http.NewRequest("GET", "/health", nil)
    if err != nil {
        t.Fatal(err)
    }
    rr := httptest.NewRecorder()
    handler := http.HandlerFunc(HealthCheckHandler)

    handler.ServeHTTP(rr, req)

    if status := rr.Code; status != http.StatusOK {
        t.Errorf("handler returned wrong status code: got %v want %v",
            status, http.StatusOK)
    }

    t.Logf("%v", rr.Header())

    if ctype := rr.Header().Get("Content-Type"); ctype != "application/json" {
        t.Errorf("content type header does not match: got %v want %v",
            ctype, "application/json")
    }
}

when I run the test, everything is ok

go test -v
=== RUN   TestHealthCheckHandler
--- PASS: TestHealthCheckHandler (0.00s)
    handlers_test.go:24: map[Content-Type:[application/json]]
PASS
ok          0.012s

the Content-Type is application/json, but when I run the service and I call it with a curl, the Content-Type is text/plain

curl -v localhost:8000/health
*   Trying ::1...
* Connected to localhost (::1) port 8000 (#0)
> GET /health HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Thu, 14 Feb 2019 01:37:15 GMT
< Content-Length: 15
< Content-Type: text/plain; charset=utf-8
<
* Connection #0 to host localhost left intact

Why the behavior is different from the test and the execution?

(the example is based on https://github.com/gorilla/mux#testing-handlers)

EDIT 1

When I changed the order between the lines, from

w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")

to

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)

with curl, I got the expected behavior, Content-Type: application/json

curl -v localhost:8000/health
*   Trying ::1...
* Connected to localhost (::1) port 8000 (#0)
> GET /health HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/json
< Date: Thu, 14 Feb 2019 01:43:18 GMT
< Content-Length: 15
<
* Connection #0 to host localhost left intact

but anyways, in the original case, why test and execution show different Content-Type?

EDIT 2

I copied a tcpdump, it contains Content-Type: text/plain.

11:51:05.686149 IP localhost.47368 > localhost.32000: Flags [P.], seq 1:92, ack 1, win 342, options [nop,nop,TS val 23544153 ecr 23544153], length 91
E...-L@.@.............}..Vl..WAu...V.......
.gAY.gAYGET /health-check HTTP/1.1
Host: localhost:32000
User-Agent: curl/7.47.0
Accept: */*


11:51:05.686847 IP localhost.32000 > localhost.47368: Flags [P.], seq 1:133, ack 92, win 342, options [nop,nop,TS val 23544153 ecr 23544153], length 132
E....-@.@.<.........}....WAu.Vln...V.......
.gAY.gAYHTTP/1.1 200 OK
Date: Thu, 14 Feb 2019 14:51:05 GMT
Content-Length: 15
Content-Type: text/plain; charset=utf-8

{"alive": true}

JuanPablo
  • 21,182
  • 32
  • 102
  • 155
  • 1
    If changing the order affected your `curl` command but didn't change the (working) `go` test then I'd say it is in `curl` somewhere. – ivanivan Feb 14 '19 at 01:50
  • 2
    Only set the status if it is non-200. Golang's http pkg set's 200 (`http.StatusOK`) by default - if none is set. Setting it - will cause a duplicate header - thus the confusion you are seeing on your http clients. – colm.anseo Feb 14 '19 at 02:05
  • @colminator thanks for your help, in "Edit 2", I copied a tcpdump output with the http request/response, this only contains one header and not a duplicate header ... so the problem could be related with the client curl? – JuanPablo Feb 14 '19 at 14:57

2 Answers2

2

simply change your function to:

func HealthCheckHandler(w http.ResponseWriter, r *http.Request) {

    // this will cause a duplicate status header to be written
    // w.WriteHeader(http.StatusOK)

    w.Header().Set("Content-Type", "application/json")

    io.WriteString(w, `{"alive": true}`)
}
colm.anseo
  • 8,213
  • 1
  • 22
  • 31
2

The issue is, as @colminator stated, you're sending your body before your header. That doesn't work in any language - it's a fact of HTTP, not of Go.

The reason it was not caught by your test is that your test is actually misusing ResponseRecorder; you're setting fields in a map then reading the fields from that map directly. Tests should only be checking against ResponseRecorder.Result, which is designed to give you the result a client would actually receive, including locking down headers when the body is sent:

if ctype := rr.Response().Header.Get("Content-Type"); ctype != "application/json" {
    t.Errorf("content type header does not match: got %v want %v",
        ctype, "application/json")
}
Adrian
  • 32,698
  • 4
  • 74
  • 74