jzxhuang / http-extras / Http.Detailed

Create HTTP requests that return more detailed responses.

I wrote a guide explaining how to extract detailed information from HTTP responses in Elm, both with and without this package. Giving it a read might help you better understand the motivation and use cases behind this module!


The metadata and original body of an HTTP response are often very useful. Maybe your server returns a useful error message you'd like to try and decode, or you're receiving an auth token in the header of a response.

Unfortunately, this information is discarded in the responses from elm/http. This module lets you create HTTP requests that keep that useful information around.

The API is designed so that usage of this module is exactly the same as using elm/http, with the only difference being that a more detailed Result is returned.

Example

Create an HTTP request like you normally would - just use this module's expect functions instead of the ones from the default package.

import Http
import Http.Detailed

type Msg
    = GotText (Result (Http.Detailed.Error String) ( Http.Metadata, String ))
    | ...

Http.get
    { url = "https://elm-lang.org/assets/public-opinion.txt"
    , expect = Http.Detailed.expectString GotText
    }

If a successful response is received, a Tuple containing the metadata and expected body is returned. You can access a header from the metadata if needed.

In case of an error, a custom Error type is returned which keeps the metadata and body around if applicable, rather than discarding them. Maybe you want to try and decode the error message!

Your update function might look a bit like this:

update msg model =
    case msg of
        GotText detailedResponse ->
            case detailedResponse of
                Ok ( metadata, text ) ->
                    -- Do something with the metadata if you need!

                Err error ->
                    case error of
                        Http.Detailed.BadStatus metadata body ->
                            -- Try to decode the body - it might be a useful error message!

                        Http.Detailed.Timeout ->
                            -- No metadata is given here - the request timed out

                        ...

        ...

Expect (Tuple)

Exactly like the expect functions from elm/http - usage of the API is the same. The difference is that the Result is more detailed.

A modified version of the examples from elm/http are included for each function to help guide you in using this module.


type Error body
    = BadUrl String
    | Timeout
    | NetworkError
    | BadStatus Http.Metadata body
    | BadBody Http.Metadata body String

Our custom error type. Similar to Http.Error, but keeps the metadata and body around in BadStatus and BadBody rather than discarding them. Maybe your API gives a useful error message you want to decode!

body can be either String or Bytes, depending on which expect function you use.

The BadBody state will only be entered when using expectJson or expectBytes.

expectString : (Result (Error String) ( Http.Metadata, String ) -> msg) -> Http.Expect msg

Expect the response body to be a String.

import Http
import Http.Detailed

type Msg
    = GotText (Result (Http.Detailed.Error String) ( Http.Metadata, String ))

getPublicOpinion : Cmd Msg
getPublicOpinion =
    Http.get
        { url = "https://elm-lang.org/assets/public-opinion.txt"
        , expect = Http.Detailed.expectString GotText
        }

On success, return a Tuple containing the metadata and the body as a String. On error, return our custom Error type.

When using this, the error will never be of type BadBody.

expectJson : (Result (Error String) ( Http.Metadata, a ) -> msg) -> Json.Decode.Decoder a -> Http.Expect msg

Expect the response body to be JSON, and try to decode it.

import Http
import Http.Detailed
import Json.Decode exposing (Decoder, field, string)

type Msg
    = GotGif (Result (Http.Detailed.Error String) ( Http.Metadata, String ))

getRandomCatGif : Cmd Msg
getRandomCatGif =
    Http.get
        { url = "https://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC&tag=cat"
        , expect = Http.Detailed.expectJson GotGif gifDecoder
        }

gifDecoder : Decoder String
gifDecoder =
    field "data" (field "image_url" string)

On success, return a Tuple containing the metadata and the decoded body. On error, return our custom Error type.

If the JSON decoder fails, you get a BadBody error that tries to explain what went wrong.

expectBytes : (Result (Error Bytes) ( Http.Metadata, a ) -> msg) -> Bytes.Decode.Decoder a -> Http.Expect msg

Expect the response body to be binary data, and try to decode it.

import Bytes exposing (Bytes)
import Bytes.Decode
import Http
import Http.Detailed

type Msg
    = GotData (Result (Http.Detailed.Error Bytes) ( Http.Metadata, Data ))

getData : Cmd Msg
getData =
    Http.get
        { url = "/data"
        , expect = Http.Detailed.expectBytes GotData dataDecoder
        }

dataDecoder : Bytes.Decode.Decoder Data

-- This is a Bytes decoder (not implemented)...

On success, return the a tuple containing the metadata and the decoded body. On error, return our custom Error type.

If the Bytes decoder fails, you get a BadBody error that just indicates that something went wrong. You can try to debug by taking a look at the bytes you are getting in the browser DevTools or something.

expectWhatever : (Result (Error Bytes) () -> msg) -> Http.Expect msg

Expect the response body to be whatever. It does not matter. Ignore it!

import Http
import Http.Detailed

type Msg
    = Uploaded (Result (Http.Detailed.Error Bytes) ())

upload : File -> Cmd Msg
upload file =
    Http.post
        { url = "/upload"
        , body = Http.fileBody file
        , expect = Http.Detailed.expectWhatever Uploaded
        }

The server may be giving back a response body, but we do not care about it.

On error, return our custom Error type. It will never be BadBody.

Expect (Record)

Prefer a record rather than a Tuple? Use these functions instead. Same as the expect functions above, but on a successful response returns a record of our custom type Success instead of a Tuple.


type alias Success body =
{ metadata : Http.Metadata
, body : body 
}

A custom type containing the full details for a successful response as a record.

body can be either String or Bytes, depending on which expect function you use.

expectStringRecord : (Result (Error String) (Success String) -> msg) -> Http.Expect msg

expectJsonRecord : (Result (Error String) (Success a) -> msg) -> Json.Decode.Decoder a -> Http.Expect msg

expectBytesRecord : (Result (Error Bytes) (Success a) -> msg) -> Bytes.Decode.Decoder a -> Http.Expect msg

Transform

These functions transform an Http.Response value into the detailed Result that is returned in each expect function in this module. You can use these to build your own expect functions.

For example, to create Http.Detailed.expectJson:

import Http
import Http.Detailed
import json.Decode


expectJson : (Result (Http.Detailed.Error String) ( Http.Metadata, a ) -> msg) -> Json.Decode.Decoder a -> Http.Expect msg
expectJson toMsg decoder =
    Http.expectStringResponse toMsg (responseToJson decoder)

Use this with Mock to mock a request with a detailed response!

import Http
import Http.Detailed
import Http.Mock


type Msg
    = GotText (Result (Http.Detailed.Error String) ( Http.Metadata, String ))
    | ...


-- Mock a request, with a Detailed Result!

Http.get
    { url = "https://fakeurl.com"
    , expect = Http.Mock.expectStringResponse Http.Timeout_ GotText Http.Detailed.responseToString
    }

responseToString : Http.Response String -> Result (Error String) ( Http.Metadata, String )

responseToJson : Json.Decode.Decoder a -> Http.Response String -> Result (Error String) ( Http.Metadata, a )

responseToBytes : Bytes.Decode.Decoder a -> Http.Response Bytes -> Result (Error Bytes) ( Http.Metadata, a )

responseToWhatever : Http.Response Bytes -> Result (Error Bytes) ()

responseToStringRecord : Http.Response String -> Result (Error String) (Success String)

responseToJsonRecord : Json.Decode.Decoder a -> Http.Response String -> Result (Error String) (Success a)

responseToBytesRecord : Bytes.Decode.Decoder a -> Http.Response Bytes -> Result (Error Bytes) (Success a)