Going Beyond 200 OK: A Guide to Detailed HTTP Responses in Elm

This tutorial covers how to extract the headers from a successful request, get the response body from a failed request, and more.

Note: As you follow this guide, it’ll probably be helpful to have the docs for elm/http open in another tab for reference. In all code examples, assume that the following import statement is included at the top of the file:

import Http exposing (..)

Motivation

Nearly every week on the Elm Slack, I see questions like these:

Frequently asked questions about HTTP in Elm, from the Elm Slack.

When I first worked with HTTP in Elm, I was in the same boat as these folks — I couldn’t figure out how to extract certain information from an HTTP response. It turns out that due to the way elm/http has been designed, this seemingly trivial task isn’t exactly straightforward!

While I’m pretty comfortable with elm/http now, I’m sure many people will run into this problem when they first work with HTTP in Elm. If that’s you, hopefully this guide will help you out! I’ll explain how to use elm/http to get detailed information from an HTTP response with full working examples at the end. Let’s get started!

Everything is (Not) Going to be 200 OK

For a simple HTTP request, we only care about the status code of the response. If it’s the magical number 200, we’ll use the response body for something useful. Otherwise, we can handle the error appropriately based on the status code.

Sadly, this isn’t always the case… source.

More often than not though, we care about HTTP responses in much greater detail. Information like the headers, response URL and response body on a failed request can be very important. Maybe our request resulted in a 500 error, but our server tells us useful information in the response body? Or maybe we pass an authentication token in a header. Whatever it may be, simply knowing the status code of the response isn’t enough — we need to go beyond 200 OK!

It’s All in the Details

Let’s begin by considering a successful HTTP response, and separate the response into two parts: the response body, and everything else.

In fact, that’s exactly what elm/http does. It calls that “everything else” the metadata, and it’s defined as follows:

With the following explanations for each field:

  • url of the server that actually responded (so you can detect redirects)
  • statusCode like 200 or 404
  • statusText describing what the statusCode means a little
  • headers like Content-Length and Expires

Seems great! The metadata has all the extra details we could possibly need — so what’s the problem?

Well, even though the metadata is defined, it’s not returned most of the time!

Understanding Elm’s HTTP Package

Wait, how could it not be returned? Let me explain.

With elm/http, we typically use the expect functions to interpret HTTP responses, namely expectString, expectJson, expectBytes, and expectWhatever. There’s plenty of examples in the package’s documentation as well as The Official Elm Guide on how to use these functions.

These functions all return a Result, containing either:

  • The Http.Error type, if our request was unsuccessful.
  • The response body, if our request was successful. The type of the body depends on which expect function we use — for example, a String if we use expectString.

Notice that the metadata is nowhere to be seen!

If we want the metadata, we need to use the functions from the Elaborate Expectations section (wow, fancy!). Specifically, the functions expectStringResponse and expectBytesResponse. We can use these to create HTTP requests that return more detailed responses. The bad news is that there isn’t too much documentation on how to use them… which is exactly why I’m writing this guide!

Elaborate Expectations

First of all, The only difference between expectStringResponse and expectBytesResponse is the way you’d like to interpret the body — is your server returning a String or a bunch of Bytes? We’ll work with expectStringResponse, as strings are a lot more readable than bytes!

Consider expectString, which is the simplest of the expect functions. Here’s its type signature:

expectString : (Result Error String -> msg) -> Expect msg

Notice that in the case of a successful request, we only get a String (the response body) — no metadata here! What we’d prefer instead is a function that has a type signature like this:

expectStringDetailed : (Result Error ( Metadata, String ) -> msg) -> Expect msg

Instead of returning only the body as a String, return both the metadata and the body as ( Metadata, String ).

We can implement this using expectStringResponse. Let’s take a look at some more type signatures from elm/http to better understand how to use expectStringResponse.

expectStringResponse allows us to send an HTTP request and return any Result (Result x a) we want. We just have to provide a function that converts a Response and into our desired Result.

The Result we’d like for now is (Result Error ( Metadata, String ). Let’s create a function that converts a Response into our desired Result, and call it convertResponseString.

On a successful HTTP request, return ( Metadata, String ) instead of just String!

In the case of a successful HTTP request, return Ok ( metadata, body ) instead of only the body! Now that we have the Result we want, pass this function to expectStringResponse to implement expectStringDetailed.

Easy! Using this function, our HTTP requests return the response body as well as the metadata! We can access the headers and response URL from the metadata. That is, if our request was successful… what if it fails?

Creating a Custom Error Type

If the HTTP request fails, expectString will return data of the type Error from elm/http.

Compare this with Response (shown earlier) and you’ll find that they’re very similar…but with some minor yet impactful differences.

Notice that theBadStatus_ case of Response includes both the metadata and the body. That’s because if the HTTP request fails with a bad status code, it still contains a response body!

However, the BadStatus case of Error only contains an Int, which is the status code of the response. The body and the rest of the metadata is discarded by elm/http!

To fix this, we can create our own custom error type that includes the entire metadata along with the body in the BadStatus case.

Let’s change expectStringDetailed’s type signature to use our custom error, DetailedError instead of Http.Error!

expectStringDetailed: (Result DetailedError ( Metadata, String ) -> msg) -> Expect msg

All that’s left to do is modify convertResponseString to return our custom error. In the BadStatus branch, we put in the complete metadata and body instead of just the status code.

Sweet! We’ve now implemented a function, expectStringDetailed, that will include all the detailed information that we might find useful in the response, like the metadata and the error body, whether or not our HTTP request was successful!

Full Examples and a Dedicated Package

I’ve created an Ellie that shows a complete example on how to use the expectStringDetailed function we created, from sending the request using Http.get to displaying the detailed response in our page.

A full end-to-end example showing how to use expectStringDetailed.

I’ve also implemented the equivalent functions expectJsonDetailed and expectBytesDetailed. I had to make the following modifications:

  • The respective convert functions take in either a JSON or Bytes decoder as the first argument.
  • The metadata and body are included in the BadBody branch of the ErrorDetailed type. This branch is entered when we try and decode JSON or Bytes and fail!
  • The ErrorDetailed type is now polymorphic, as the body could be either String or Bytes depending on which expect function is used.
Full example including implementation of expectJsonDetailed and expectBytesDetailed

Finally, I have published a package that contains all of these functions, so you don’t have to implement them yourself every time. In addition to the functions discussed, you can also get the detailed response as a record, mock API responses, and more! Here’s an Ellie that does the exact thing as the previous example, but using my package:

Full example showing how to handle detailed responses using jzxhuang/http-extras.

Conclusion

More often than not, we need to go beyond 200 OK. We require more than just the response body on a successful request and the status code on a failed request. In Elm, getting these details it isn’t so straightforward — you need to use the ‘elaborate’ functions expectStringResponse and expectBytesResponse to return a more detailed Result.

It’s a bit tedious and unintuitive to do this all the time, which leads me to wonder if it was worth simplifying the expect functions at the cost of this extra hassle. As I mentioned at the beginning, many people are running into the same problem! It’s definitely an interesting point of discussion…

More often than not, we need to go beyond 200 OK.

Hopefully, this guide helped you understand how to handle HTTP responses in detail with elm/http. Again, my package jzxhuang/http-extras includes everything in this guide and more. Other great packages for working with HTTP include jinjor/elm-req (focuses on tasks) and rakutentech/http-trinity (includes an interesting way of mocking requests, which I also contributed to).

Thanks for reading, and if you have any questions or comments, post away!