Let's Go Processing forms › Parsing form data
Previous · Contents · Next
Chapter 8.2.

Parsing form data

Thanks to the work we did previously in the advanced routing section, any POST /snippets/create requests are already being dispatched to our snippetCreatePost handler. We’ll now update this handler to process and use the form data when it’s submitted.

At a high-level we can break this down into two distinct steps.

  1. First, we need to use the r.ParseForm() method to parse the request body. This checks that the request body is well-formed, and then stores the form data in the request’s r.PostForm map. If there are any errors encountered when parsing the body (like there is no body, or it’s too large to process) then it will return an error. The r.ParseForm() method is also idempotent; it can safely be called multiple times on the same request without any side-effects.

  2. We can then get to the form data contained in r.PostForm by using the r.PostForm.Get() method. For example, we can retrieve the value of the title field with r.PostForm.Get("title"). If there is no matching field name in the form this will return the empty string "", similar to the way that query string parameters worked earlier in the book.

Open your cmd/web/handlers.go file and update it to include the following code:

File: cmd/web/handlers.go
package main

...

func (app *application) snippetCreatePost(w http.ResponseWriter, r *http.Request) {
    // First we call r.ParseForm() which adds any data in POST request bodies
    // to the r.PostForm map. This also works in the same way for PUT and PATCH
    // requests. If there are any errors, we use our app.ClientError() helper to 
    // send a 400 Bad Request response to the user.
    err := r.ParseForm()
    if err != nil {
        app.clientError(w, http.StatusBadRequest)
        return
    }

    // Use the r.PostForm.Get() method to retrieve the title and content
    // from the r.PostForm map.
    title := r.PostForm.Get("title")
    content := r.PostForm.Get("content")

    // The r.PostForm.Get() method always returns the form data as a *string*.
    // However, we're expecting our expires value to be a number, and want to
    // represent it in our Go code as an integer. So we need to manually covert
    // the form data to an integer using strconv.Atoi(), and we send a 400 Bad
    // Request response if the conversion fails.
    expires, err := strconv.Atoi(r.PostForm.Get("expires"))
    if err != nil {
        app.clientError(w, http.StatusBadRequest)
        return
    }

    id, err := app.snippets.Insert(title, content, expires)
    if err != nil {
        app.serverError(w, err)
        return
    }

    http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther)
}

Alright, let’s give this a try! Restart the application and try filling in the form with the title and content of a snippet, a bit like this:

08.02-01.png

And then submit the form. If everything has worked, you should be redirected to a page displaying your new snippet like so:

08.02-02.png

Additional information

The r.Form map

In our code above, we accessed the form values via the r.PostForm map. But an alternative approach is to use the (subtly different) r.Form map.

The r.PostForm map is populated only for POST, PATCH and PUT requests, and contains the form data from the request body.

In contrast, the r.Form map is populated for all requests (irrespective of their HTTP method), and contains the form data from any request body and any query string parameters. So, if our form was submitted to /snippet/create?foo=bar, we could also get the value of the foo parameter by calling r.Form.Get("foo"). Note that in the event of a conflict, the request body value will take precedent over the query string parameter.

Using the r.Form map can be useful if your application sends data in a HTML form and in the URL, or you have an application that is agnostic about how parameters are passed. But in our case those things aren’t applicable. We expect our form data to be sent in the request body only, so it’s sensible for us to access it via r.PostForm.

The FormValue and PostFormValue methods

The net/http package also provides the methods r.FormValue() and r.PostFormValue(). These are essentially shortcut functions that call r.ParseForm() for you, and then fetch the appropriate field value from r.Form or r.PostForm respectively.

I recommend avoiding these shortcuts because they silently ignore any errors returned by r.ParseForm(). That’s not ideal — it means our application could be encountering errors and failing for users, but there’s no feedback mechanism to let them know.

Multiple-value fields

Strictly speaking, the r.PostForm.Get() method that we’ve used above only returns the first value for a specific form field. This means you can’t use it with form fields which potentially send multiple values, such as a group of checkboxes.

<input type="checkbox" name="items" value="foo"> Foo
<input type="checkbox" name="items" value="bar"> Bar
<input type="checkbox" name="items" value="baz"> Baz

In this case you’ll need to work with the r.PostForm map directly. The underlying type of the r.PostForm map is url.Values, which in turn has the underlying type map[string][]string. So, for fields with multiple values you can loop over the underlying map to access them like so:

for i, item := range r.PostForm["items"] {
    fmt.Fprintf(w, "%d: Item %s\n", i, item)
}

Limiting form size

Unless you’re sending multipart data (i.e. your form has the enctype="multipart/form-data" attribute) then POST, PUT and PATCH request bodies are limited to 10MB. If this is exceeded then r.ParseForm() will return an error.

If you want to change this limit you can use the http.MaxBytesReader() function like so:

// Limit the request body size to 4096 bytes
r.Body = http.MaxBytesReader(w, r.Body, 4096)

err := r.ParseForm()
if err != nil {
    http.Error(w, "Bad Request", http.StatusBadRequest)
    return
}

With this code only the first 4096 bytes of the request body will be read during r.ParseForm(). Trying to read beyond this limit will cause the MaxBytesReader to return an error, which will subsequently be surfaced by r.ParseForm().

Additionally — if the limit is hit — MaxBytesReader sets a flag on http.ResponseWriter which instructs the server to close the underlying TCP connection.