Going Beyond 200 OK: A Guide to Detailed HTTP Responses in Elm
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:

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.

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
like200
or404
statusText
describing what thestatusCode
means a littleheaders
likeContent-Length
andExpires
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, aString
if we useexpectString
.
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
.
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.
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 theErrorDetailed
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 eitherString
orBytes
depending on whichexpect
function is used.
expectJsonDetailed
and expectBytesDetailedFinally, 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:
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!