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:
|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:
Although this change looks straightforward there are a couple of nuances I should explain:
It’s only possible to call
w.WriteHeader()once per response, and after the status code has been written it can’t be changed. If you try to call
w.WriteHeader()a second time Go will log a warning message.
If you don’t call
w.WriteHeader()explicitly, then the first call to
w.Write()will automatically send a
200 OKstatus code to the user. So, if you want to send a non-200 status code, you must call
w.WriteHeader()before any call to
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:
But if you use a different request method — like
DELETE — you should now get response with a
405 Method Not Allowed status code. For example:
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:
Let’s take a look at this in action again by sending a non-
POST request to our
/snippet/create URL, like so:
Notice how the response now includes a
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.Write() methods behind-the-scenes for us.
Let’s update the code to use this instead.
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.WriteHeader() methods directly like we have been doing so far. But I wanted to introduce them upfront because they underpin the more advanced (and interesting!) ways to send responses.
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
Get() methods that you can use to read and manipulate the header map too.
System-Generated Headers and Content Sniffing
When sending a response Go will automatically set three system-generated headers for you:
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.
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. 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 like so:
When you’re using the
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:
Suppressing System-Generated Headers
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: