You're reading a sample of this book. Grab the full version here.

2.5. Customizing HTTP Headers

Let's now update our application so that the /snippet/create route only responds to HTTP requests which use the POST method, like so:

Method Pattern Handler Action
ANY / home Display the home page
ANY /snippet showSnippet Display a specific snippet
POST /snippet/create createSnippet Create a new snippet

Making this change is important because — later in our application build — requests to the /snippet/create route will result in a new snippet being created in a database. Creating a new snippet in a database is a non-idempotent action that changes the state of our server, so we should follow HTTP good practice and restrict this route to act on POST requests only.

But the main reason I want to cover this now is because it's a good excuse to talk about HTTP response headers and explain how to customize them.

HTTP Status Codes

Let's begin by updating our createSnippet() handler function so that it sends a 405 (method not allowed) HTTP status code unless the request method is POST. To do this we'll need to use the w.WriteHeader() method like so:

File: main.go
package main

...

func createSnippet(w http.ResponseWriter, r *http.Request) {
    // Use r.Method to check whether the request is using POST or not.
    // If it's not, use the w.WriteHeader() method to send a 405 status code and
    // the w.Write() method to write a "Method Not Allowed" response body. We
    // then return from the function so that the subsequent code is not executed.
    if r.Method != "POST" {
        w.WriteHeader(405)
        w.Write([]byte("Method Not Allowed"))
        return
    }

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

...

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

Let's take a look at this in action.

Restart the server, then open a second terminal window and use curl to make a POST request to http://localhost:4000/snippet/create. You should get a HTTP response with a 200 OK status code similar to this:

$ curl -i -X POST http://localhost:4000/snippet/create
HTTP/1.1 200 OK
Date: Thu, 02 Aug 2018 12:58:54 GMT
Content-Length: 23
Content-Type: text/plain; charset=utf-8

Create a new snippet...

But if you use a different request method — like GET, PUT or DELETE — you should now get response with a 405 Method Not Allowed status code. For example:

$ curl -i -X PUT http://localhost:4000/snippet/create
HTTP/1.1 405 Method Not Allowed
Date: Thu, 02 Aug 2018 12:59:16 GMT
Content-Length: 18
Content-Type: text/plain; charset=utf-8

Method Not Allowed

Customizing Headers

Another improvement we can make is to include an Allow: POST header with every 405 Method Not Allowed response to let the user know which request methods are supported for that particular URL.

We can do this by using the w.Header().Set() method to add a new header to the response header map, like so:

File: main.go
package main

...

func createSnippet(w http.ResponseWriter, r *http.Request) {
    if r.Method != "POST" {
        // Use the Header().Set() method to add an 'Allow: POST' header to the
        // response header map. The first parameter is the header name, and
        // the second parameter is the header value.
        w.Header().Set("Allow", "POST")
        w.WriteHeader(405)
        w.Write([]byte("Method Not Allowed"))
        return
    }

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

...

It's important to point out here that changing the header map after a call to w.WriteHeader() or w.Write() will have no effect on the response headers that the user receives. You need to make sure that your header map contains all the headers you want before you call these methods.

Let's take a look at this in action again by sending a non-POST request to our /snippet/create URL, like so:

$ curl -i -X PUT http://localhost:4000/snippet/create
HTTP/1.1 405 Method Not Allowed
Allow: POST
Date: Thu, 02 Aug 2018 13:01:16 GMT
Content-Length: 18
Content-Type: text/plain; charset=utf-8

Method Not Allowed

Notice how the response now includes our Allow: POST header?

The http.Error Shortcut

If you want to send a non-200 status code and a plain-text response body (like we are in the code above) then it's a good opportunity to use the http.Error() shortcut. This is a lightweight helper function which takes a given message and status code, then calls the w.WriteHeader() and w.Write() methods behind-the-scenes for us.

Let's update the code to use this instead.

File: main.go
package main

...

func createSnippet(w http.ResponseWriter, r *http.Request) {
    if r.Method != "POST" {
        w.Header().Set("Allow", "POST")
        // Use the http.Error() function to send a 405 status code and "Method Not
        // Allowed" string as the response body.
        http.Error(w, "Method Not Allowed", 405)
        return
    }

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

...

In terms of functionality this is almost exactly the same. The biggest difference is that we're now passing our http.ResponseWriter to another function, which sends a response to the user for us.

The pattern of passing http.ResponseWriter to other functions is super-common in Go, and something we'll do a lot throughout this book. In practice, it's quite rare to use the w.Write() and w.WriteHeader() methods directly like we have been so far. But I wanted to introduce them upfront because they underpin the more advanced (and interesting!) ways to send responses.


Additional Information

Manipulating the Header Map

In the code above we used w.Header.Set() to add a new header to the response header map. But there's also Add(), Del() and Get() methods that you can use to read and manipulate 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")

System-Generated Headers and Content

When sending a response Go will automatically set three system-generated headers for you: Date and Content-Length and Content-Type.

The Content-Type header is particularly interesting. Go will attempt to set the correct one for you by content sniffing 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 new to Go is that it can't distinguish JSON from plain text. And, 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 like so:

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

Header Canonicalization

When you're using the Add(), Get(), Set() and Del() 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). For example:

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

Note: When headers are written to a HTTP/2 connection the header names and values will always be converted to lowercase, as per the specifications.

Suppressing System-Generated Headers

The Del() method doesn't remove system-generated headers. To suppress these, you need to access the underlying header map directly and set the value to nil. If you want to suppress the Date header, for example, you need to write:

w.Header()["Date"] = nil