66. Your first HTTP handler
Go is the language people reach for when they need a network service, and the
reason is net/http: a production-grade HTTP server in the standard library,
with no framework to pick and nothing to install. This module builds a real
JSON service out of it, one piece at a time.
Everything starts with one function signature.
A handler is just a function
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello from a Go handler")
}
func main() {
req := httptest.NewRequest("GET", "/hello", nil)
rec := httptest.NewRecorder()
http.HandlerFunc(hello).ServeHTTP(rec, req)
fmt.Println("status:", rec.Code)
fmt.Print("body: ", rec.Body.String())
}
status: 200
body: hello from a Go handler
hello is the whole thing. Two arguments, no return value:
w http.ResponseWriter— where the response goes. It's anio.Writer, which is whyfmt.Fprintlnworks on it: everything you learned about writers in module 11 applies here unchanged.r *http.Request— everything the client sent. A pointer, because a request is large and you never want a copy.
Nothing is returned. You write the response; you don't hand one back.
Why there's no server in that program
You'd normally start a server with http.ListenAndServe and hit it with a
browser. These lesson boxes run inside your browser tab, which has no port to
listen on — so instead we call the handler directly:
httptest.NewRequestbuilds a*http.Requestin memory.httptest.NewRecorderreturns aResponseWriterthat records what the handler wrote instead of sending it anywhere.ServeHTTP(rec, req)invokes the handler with that pair.
This isn't a lesson-player workaround you'll throw away later. Driving a
handler directly, with no socket in the middle, is exactly how Go programmers
test HTTP code — it's the whole point of the httptest package, and it's what
the last lesson of this module does deliberately. You're learning the real
technique early because it's also the clearest way to see a handler work.
The real server shows up in lesson 6.
http.Handler is the interface
http.HandlerFunc(hello) in that program is a conversion, and it's worth
understanding rather than copying. The interface the server actually wants is:
type Handler interface {
ServeHTTP(w http.ResponseWriter, r *http.Request)
}
A plain function doesn't have methods, so it can't satisfy that. HandlerFunc
is a named function type in net/http with a ServeHTTP method that calls
itself — the adapter trick from module 6, in the standard library:
type HandlerFunc func(http.ResponseWriter, *http.Request)
func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
f(w, r)
}
So http.HandlerFunc(hello) says "treat this function as a Handler". Any type
with a ServeHTTP method works just as well, which is how you give a handler
dependencies:
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
type greeter struct {
greeting string
}
func (g greeter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s, %s!", g.greeting, r.URL.Query().Get("name"))
}
func main() {
var h http.Handler = greeter{greeting: "Hello"}
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest("GET", "/?name=Ada", nil))
fmt.Println(rec.Body.String())
}
Hello, Ada!
A struct with a ServeHTTP method is the standard answer to "my handler needs
a database connection". Put the dependency in the struct; the method reads it.
No globals, no framework.
Reading the request
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
func describe(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "method=%s\npath=%s\nquery=%q\nheader=%q\n",
r.Method, r.URL.Path, r.URL.Query().Get("q"), r.Header.Get("X-Request-Id"))
}
func main() {
req := httptest.NewRequest("POST", "/search?q=slices&page=2", nil)
req.Header.Set("X-Request-Id", "abc-123")
rec := httptest.NewRecorder()
http.HandlerFunc(describe).ServeHTTP(rec, req)
fmt.Print(rec.Body.String())
}
method=POST
path=/search
query="slices"
header="abc-123"
The four you'll use constantly:
| field | what it gives you |
|---|---|
r.Method |
"GET", "POST", … — a plain string |
r.URL.Path |
the path only, without the query string |
r.URL.Query() |
parsed query params; .Get(k) returns "" if absent |
r.Header.Get(k) |
one header, case-insensitively |
Two things that catch people out. r.URL.Path really does exclude the query —
notice the output says /search, not /search?q=slices&page=2. And
Query().Get returns the empty string for a missing key rather than an error,
so "absent" and "present but empty" look identical; when the difference
matters, use r.URL.Query()["q"] and check the length.
r.Body is an io.ReadCloser, so reading JSON out of it is exactly the
io work from module 11. That's lesson 3.
Writing the response
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
func created(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Location", "/items/7")
w.WriteHeader(http.StatusCreated)
fmt.Fprint(w, "created item 7")
}
func main() {
rec := httptest.NewRecorder()
http.HandlerFunc(created).ServeHTTP(rec, httptest.NewRequest("POST", "/items", nil))
fmt.Println("status: ", rec.Code)
fmt.Println("location:", rec.Header().Get("Location"))
fmt.Println("type: ", rec.Header().Get("Content-Type"))
fmt.Println("body: ", rec.Body.String())
}
status: 201
location: /items/7
type: text/plain; charset=utf-8
body: created item 7
Use the named constants — http.StatusCreated, not 201. They read better
and they're impossible to typo into a different valid code.
The ordering rule
Headers, then status, then body — in that order, once. This is the single most common way to get HTTP wrong in Go, because getting it wrong doesn't produce an error. It just silently does nothing:
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
func tooLate(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "starting...")
w.WriteHeader(http.StatusInternalServerError)
w.Header().Set("X-Late", "yes")
}
func main() {
rec := httptest.NewRecorder()
http.HandlerFunc(tooLate).ServeHTTP(rec, httptest.NewRequest("GET", "/", nil))
fmt.Println("rec.Code: ", rec.Code)
fmt.Println("live header X-Late: ", rec.Header().Get("X-Late"))
fmt.Println("sent header X-Late: ", rec.Result().Header.Get("X-Late"))
fmt.Println("sent status: ", rec.Result().StatusCode)
}
rec.Code: 200
live header X-Late: yes
sent header X-Late:
sent status: 200
The first Write commits the response: it sends a 200 for you, along
with whatever headers were set at that moment. Everything after that is too
late. The status stayed 200 even though the handler asked for 500, and the
header set afterwards never went out.
That output also shows the difference between two things on the recorder that are easy to confuse:
rec.Header()is the live header map — still writable, soX-Lateappears in it.rec.Result()is the response as it would have gone over the wire, captured at the moment the response was committed.X-Lateis absent.
When you're asserting on headers in a test, rec.Result().Header is the honest
one. Real Go also logs http: superfluous response.WriteHeader call to the
server log when you do this — a good thing to recognise, since it points at
exactly this bug.
The fix is always the same shape: decide everything, then write. An early
return after an error response is what keeps that true.
Your turn
Write a handler that reads a name query parameter. If it's missing, respond
400 with the body name is required. Otherwise set a Content-Type of
text/plain and respond hello, <name>.
Mind the ordering rule — the status has to be set before anything is written.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
func status(w http.ResponseWriter, r *http.Request) {
// read the "name" query param
// if empty: 400 with body "name is required", then return
// otherwise: Content-Type text/plain, body "hello, <name>"
}
func main() {
for _, target := range []string{"/status", "/status?name=Grace"} {
rec := httptest.NewRecorder()
http.HandlerFunc(status).ServeHTTP(rec, httptest.NewRequest("GET", target, nil))
fmt.Printf("%-22s %d %s\n", target, rec.Code, rec.Body.String())
}
}
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
func status(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, "name is required")
return
}
w.Header().Set("Content-Type", "text/plain")
fmt.Fprintf(w, "hello, %s", name)
}
func main() {
for _, target := range []string{"/status", "/status?name=Grace"} {
rec := httptest.NewRecorder()
http.HandlerFunc(status).ServeHTTP(rec, httptest.NewRequest("GET", target, nil))
fmt.Printf("%-22s %d %s\n", target, rec.Code, rec.Body.String())
}
}
Next: one handler is a program. Several handlers need a router.