So far we've just assumed that all the HTTP requests our server receives are good. And if there's something that you should never do when writing web servers is trusting your input!
In this chapter we'll see how to extract the information sent in different parts of the HTTP request. Once we have that information we'll see how can validate them, and how we can signal different errors.
Let's start!
We've seen how we can route a request to different handlers depending on the
path. Now we're going to see how to extract the keys and values in the query
part of the request, aka the data after ?
.
The http.Request
type has a method FormValue
with the following docs:
func (r *Request) FormValue(key string) string
FormValue returns the first value for the named component of the query.
POST and PUT body parameters take precedence over URL query string values.
FormValue calls ParseMultipartForm and ParseForm if necessary and ignores
any errors returned by these functions. If key is not present, FormValue
returns the empty string. To access multiple values of the same key, call
ParseForm and then inspect Request.Form directly.
That's easy! So if we want to obtain the value of a parameter name
in the URL
/hello?name=jonathan
we can write the next program.
func paramHandler(w http.ResponseWriter, r *http.Request) {
name := r.FormValue("name")
if name == "" {
name = "friend"
}
fmt.Fprintf(w, "Hello, %s!", name)
}
-
Write a web server that will answer to requests to
/hello?name=world
with an HTTP response with the textHello, world!
. If thename
is not present it should printHello, friend!
.You can test it with your own browser, but let's try a couple things with
curl
too. If you haven't installedcurl
on your machine yet, now it's the time to change that. Before running these think about what you expect to see and why.$ curl "localhost:8080/hello?name=world" $ curl "localhost:8080/hello?name=world&name=francesc" $ curl -X POST -d "name=francesc" "localhost:8080/hello" $ curl -X POST -d "name=francesc" "localhost:8080/hello?name=potato"
-
How would you make your program print all the values given to
name
?
Similarly to how we read from the Body
in the http.Response
a couple of chapter before we can read
the body of the http.Request
.
Note that even though the type of Body
in http.Request
is io.ReadCloser
the body will be automatically
closed at the end of the execution of the http handler, so don't worry about it.
There's many ways we can read from an io.Reader
, but for now you can use ioutil.ReadAll
,
which returns a []byte
and an error
if something goes wrong.
func bodyHandler(w http.ResponseWriter, r *http.Request) {
b, err := ioutil.ReadAll(r.Body)
if err != nil {
fmt.Fprintf(w, "could not read body: %v", err)
return
}
name := string(b)
if name == "" {
name = "friend"
}
fmt.Fprintf(w, "Hello, %s!", name)
}
-
Modify the previous exercise so instead of reading the
name
argument from a query or form value it will use whatever content of theBody
is. Again, if theBody
is empty the response should greetfriend
.If the call to
ioutil.ReadAll
returns an error write that error message to the output.You can test your exercise by using
curl
:bash $ curl -X POST -d "francesc" "localhost:8080/hello"
-
As an extra exercise, remove any extra blank spaces surrounding the name.
In the previous exercise we decided to just print the error if the call to
ioutil.ReadAll
failed. That is actually a pretty horrible idea, as you might
imagine 😁.
How should we do it? Well, the HTTP protocol defines a set of status codes that
help us describe the nature of a response. We've actually used them before,
when we checked if the response obtained using Get
was OK
or not.
There are two ways of setting the status code with a ResponseWriter
.
Using the WriteHeader
method in ResponseWriter
we can set the status code
of the response. The parameter is an int
so you could pass any number, but it
is better to use the constants already defined in the http
package. They all
start with Status
and you can find them
here.
By default, the status code of a response will be StatusOK
aka 200
.
-
Modify your previous program so the response in case of error will have status code
500
. Instead of using the number500
find the corresponding constant. -
Then make the status of the response be
400
when the body is empty.
In the previous exercise you've noticed that often when we set the status code
of the response we also write the description of the error. That's why the
http.Error
function exists.
func Error(w ResponseWriter, error string, code int)
Error replies to the request with the specified error message and HTTP
code. The error message should be plain text.
A call to Error
can then replace a call to WriteHeader
followed by a some
call writing to the ResponseWriter
.
- Replace your calls to
WriteHeader
andFprintf
in the previous exercise with a call toError
.
You might have noticed that if you send more than one line in the response, your browser shows it as one. Why is that?
The answer is that the net/http
packages guesses that the output is HTML, and
therefore your browser concatenates the lines. For short outputs, it's hard to
guess. You can see the content type of your response by adding -v
to your
curl
command.
$ curl -v localhost:8080
< HTTP/1.1 200 OK
< Date: Mon, 25 Apr 2016 16:14:46 GMT
< Content-Length: 19
< Content-Type: text/html; charset=utf-8
So, how do we stop the net/http
package from guessing the content type? We
specify it! To do so we need to set the header "Content-Type"
to the value
"text/plain"
. You can set headers in the response with the Header
function
in the ResponseWriter
.
Header
returns a http.Header
which has, among other methods, the method Set
. We can then set the content
type in our ResponseWriter
named w
like this.
w.Header().Set("Content-Type", "text/plain")
- Set the
"Content-Type"
to"text/plain"
and verify the result withcurl
.
Imagine if you had hundreds of different http handlers, and you want to set the content type header in each and every one of them. That sounds painful, doesn't it?
Let me tell you about a cool technique that helps defining behavior shared by many handlers (in Python they are called decorators).
To start we're going to define a new type named textHandler
that contains a
http.HandlerFunc
.
type textHandler struct {
h http.HandlerFunc
}
Now we're going to define the ServeHTTP
method on textHandler
so it
satisfies the http.Handler
interface.
func (t textHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Set the content type
w.Header().Set("Content-Type", "text/plain")
// Then call ServeHTTP in the decorated handler.
t.h(w, r)
}
Finally we replace our http.HandleFunc
calls with http.Handle
.
func main() {
http.Handle("/hello", textHandler{helloHandler})
http.ListenAndServe(":8080", nil)
}
-
Modify the program from your previous exercise so you have only one line that sets the content type of the response.
-
Optional/at home: Modify the
textHandler
that we showed before so instead ofhttp.HandlerFunc
it receives a function that returns anint
and anerror
. TheServeHTTP
method oftextHandler
should check use that integer and theerror
and set the status code and content accordingly. -
Optional/at home: Define a new error type that contains the information about the status code too.
You're now able to validate the input to your http handlers and set the status code and content type accordingly.
Go to section 4 to continue.