You're reading a sample of this book. Get the full version here.
Let's Go Foundations › Customizing responses
Previous · Contents · Next
Chapter 2.6.

Customizing responses

By default, every response that your handlers send has the HTTP status code 200 OK (which indicates to the user that their request was received and processed successfully), plus three automatic system-generated headers: a Date header, and the Content-Length and Content-Type of the response body. For example:

$ curl -i localhost:4000/
HTTP/1.1 200 OK
Date: Wed, 18 Mar 2024 11:29:23 GMT
Content-Length: 21
Content-Type: text/plain; charset=utf-8

Hello from Snippetbox

In this chapter we’ll dive in to how to customize the response headers your handlers send, and also look at a couple of other ways that you can send plain text responses to users.

HTTP status codes

First of all, let’s update our snippetCreatePost handler so that it sends a 201 Created status code rather than 200 OK. To do this, you can use the w.WriteHeader() method in your handlers like so:

File: main.go
package main

...

func snippetCreatePost(w http.ResponseWriter, r *http.Request) {
    // Use the w.WriteHeader() method to send a 201 status code.
    w.WriteHeader(201)

    // Then w.Write() method to write the response body as normal.
    w.Write([]byte("Save a new snippet..."))
}

...

(Yes, this is a bit silly because the handler isn’t actually creating anything yet! But it nicely illustrates the pattern to set a custom status code.)

Although this change looks straightforward, there are a couple of nuances I should explain:

Restart the server, then use curl to make a POST request to http://localhost:4000/snippet/create again. You should see that the HTTP response now has a 201 Created status code similar to this:

$ curl -i -d "" http://localhost:4000/snippet/create
HTTP/1.1 201 Created
Date: Wed, 18 Mar 2024 11:29:23 GMT
Content-Length: 21
Content-Type: text/plain; charset=utf-8

Save a new snippet...

Status code constants

The net/http package provides constants for HTTP status codes, which we can use instead of writing the status code number ourselves. Using these constants is good practice because it helps prevent mistakes due to typos, and it can also help make your code clearer and self-documenting — especially when dealing with less-commonly-used status codes.

Let’s update our snippetCreatePost handler to use the constant http.StatusCreated instead of the integer 201, like so:

File: main.go
package main

...

func snippetCreatePost(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusCreated)

    w.Write([]byte("Save a new snippet..."))
}

...

Customizing headers

You can also customize the HTTP headers sent to a user by changing the response header map. Probably the most common thing you’ll want to do is include an additional header in the map, which you can do using the w.Header().Add() method.

To demonstrate this, let’s add a Server: Go header to the response that our home handler sends. If you’re following along, go ahead and update the handler code like so:

File: main.go
package main

...

func home(w http.ResponseWriter, r *http.Request) {
    // Use the Header().Add() method to add a 'Server: Go' header to the
    // response header map. The first parameter is the header name, and
    // the second parameter is the header value.
    w.Header().Add("Server", "Go")

    w.Write([]byte("Hello from Snippetbox"))
}

...

Let’s try this out by using curl to make another request to http://localhost:4000/. This time you should see that the response now includes a new Server: Go header, like so:

$ curl -i http://localhost:4000
HTTP/1.1 200 OK
Server: Go
Date: Wed, 18 Mar 2024 11:29:23 GMT
Content-Length: 21
Content-Type: text/plain; charset=utf-8

Hello from Snippetbox

Writing response bodies

So far in this book we’ve been using w.Write() to send specific HTTP response bodies to a user. And while this is the simplest and most fundamental way to send a response, in practice it’s far more common to pass your http.ResponseWriter value to another function that writes the response for you.

In fact, there are a lot of functions that you can use to write a response!

The key thing to understand is this… because the http.ResponseWriter value in your handlers has a Write() method, it satisfies the io.Writer interface.

If you’re new to Go, then the concept of interfaces can be a bit confusing and I don’t want to get too hung up on it right now. But at a practical level, it means that any functions where you see an io.Writer parameter, you can pass in your http.ResponseWriter value and whatever is being written will subsequently be sent as the body of the HTTP response.

That means you can use standard library functions like io.WriteString() and the fmt.Fprint*() family (all of which accept an io.Writer parameter) to write plain-text response bodies too.

// Instead of this...
w.Write([]byte("Hello world"))

// You can do this...
io.WriteString(w, "Hello world")
fmt.Fprint(w, "Hello world")

Let’s leverage this, and update the code in our snippetView handler to use the fmt.Fprintf() function. This will allow us to interpolate the wildcard id value in our response body message and write the response in a single line, like so:

File: main.go
package main

...

func snippetView(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil || id < 1 {
        http.NotFound(w, r)
        return
    }

    fmt.Fprintf(w, "Display a specific snippet with ID %d...", id)
}

...

Additional information

Content sniffing

In order to automatically set the Content-Type header, Go content sniffs the response body with the http.DetectContentType() function. If this function can’t guess the content type, Go will fall back to setting the header Content-Type: application/octet-stream instead.

The http.DetectContentType() function generally works quite well, but a common gotcha for web developers is that it can’t distinguish JSON from plain text. So, by default, JSON responses will be sent with a Content-Type: text/plain; charset=utf-8 header. You can prevent this from happening by setting the correct header manually in your handler like so:

w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"name":"Alex"}`))

Manipulating the header map

In this chapter we used w.Header().Add() to add a new header to the response header map. But there are also Set(), Del(), Get() and Values() methods that you can use to manipulate and read from the header map too.

// Set a new cache-control header. If an existing "Cache-Control" header exists
// it will be overwritten.
w.Header().Set("Cache-Control", "public, max-age=31536000")

// In contrast, the Add() method appends a new "Cache-Control" header and can
// be called multiple times.
w.Header().Add("Cache-Control", "public")
w.Header().Add("Cache-Control", "max-age=31536000")

// Delete all values for the "Cache-Control" header.
w.Header().Del("Cache-Control")

// Retrieve the first value for the "Cache-Control" header.
w.Header().Get("Cache-Control")

// Retrieve a slice of all values for the "Cache-Control" header.
w.Header().Values("Cache-Control")

Header canonicalization

When you’re using the Set(), Add(), Del(), Get() and Values() methods on the header map, the header name will always be canonicalized using the textproto.CanonicalMIMEHeaderKey() function. This converts the first letter and any letter following a hyphen to upper case, and the rest of the letters to lowercase. This has the practical implication that when calling these methods the header name is case-insensitive.

If you need to avoid this canonicalization behavior, you can edit the underlying header map directly. It has the type map[string][]string behind the scenes. For example:

w.Header()["X-XSS-Protection"] = []string{"1; mode=block"}