Http

Send HTTP requests.

get : { url : String, expect : Expect msg } -> Cmd msg

Create a GET request.

import Http

type Msg
  = GotText (Result Http.Error String)

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

You can use functions like expectString and expectJson to interpret the response in different ways. In this example, we are expecting the response body to be a String containing the full text of Public Opinion by Walter Lippmann.

Note: Use gren-lang/url to build reliable URLs.

post :
{ url : String
, body : Body
, expect : Expect msg
}
-> Cmd msg

Create a POST request. So imagine we want to send a POST request for some JSON data. It might look like this:

import Http
import Json.Decode exposing (array, string)

type Msg
  = GotBooks (Result Http.Error (Array String))

postBooks : Cmd Msg
postBooks =
  Http.post
    { url = "https://example.com/books"
    , body = Http.emptyBody
    , expect = Http.expectJson GotBooks (array string)
    }

Notice that we are using expectJson to interpret the response as JSON. You can learn more about how JSON decoders work here in the guide.

We did not put anything in the body of our request, but you can use functions like stringBody and jsonBody if you need to send information to the server.

request :
{ method : String
, headers : Array Header
, url : String
, body : Body
, expect : Expect msg
, timeout : Maybe Float
, tracker : Maybe String
}
-> Cmd msg

Create a custom request. For example, a PUT for files might look like this:

import File
import Http

type Msg = Uploaded (Result Http.Error {})

upload : File.File -> Cmd Msg
upload file =
  Http.request
    { method = "PUT"
    , headers = []
    , url = "https://example.com/publish"
    , body = Http.fileBody file
    , expect = Http.expectWhatever Uploaded
    , timeout = Nothing
    , tracker = Nothing
    }

It lets you set custom headers as needed. The timeout is the number of milliseconds you are willing to wait before giving up. The tracker lets you cancel and track requests.

Header

Body

type Body

Represents the body of a Request.

emptyBody : Body

Create an empty body for your Request. This is useful for GET requests and POST requests where you are not sending any data.

stringBody : String -> String -> Body

Put some string in the body of your Request. Defining jsonBody looks like this:

import Json.Encode as Encode

jsonBody : Encode.Value -> Body
jsonBody value =
  stringBody "application/json" (Encode.encode 0 value)

The first argument is a MIME type of the body. Some servers are strict about this!

jsonBody : Value -> Body

Put some JSON value in the body of your Request.

Maybe you want to search for 10 books relevant to a certain topic:

import Http
import Json.Encode as E

searchForBooks : String -> Cmd Msg
searchForBooks topic =
  Http.post
    { url = "https://api.example.com/books"
    , body =
        Http.jsonBody <|
          E.object
            [ ( "topic", E.string topic )
            , ( "limit", E.int 10 )
            ]
    , expect =
        Http.expectJson GotBooks booksDecoder
    }

Note: This will automatically add the Content-Type: application/json header.

fileBody : File -> Body

Use a file as the body of your Request. When someone uploads an image into the browser with gren-lang/file you can forward it to a server.

This will automatically set the Content-Type to the MIME type of the file.

Note: Use track to track upload progress.

bytesBody : String -> Bytes -> Body

Put some Bytes in the body of your Request. This allows you to use gren-lang/bytes to have full control over the binary representation of the data you are sending. For example, you could create an archive.zip file and send it along like this:

import Bytes exposing (Bytes)

zipBody : Bytes -> Body
zipBody bytes =
  bytesBody "application/zip" bytes

The first argument is a MIME type of the body. In other scenarios you may want to use MIME types like image/png or image/jpeg instead.

Note: Use track to track upload progress.

Body Parts

multipartBody : Array Part -> Body

When someone clicks submit on the <form>, browsers send a special HTTP request with all the form data. Something like this:

POST /test.html HTTP/1.1
Host: example.org
Content-Type: multipart/form-data;boundary="7MA4YWxkTrZu0gW"

--7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="title"

Trip to London
--7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="album[]"; filename="parliment.jpg"

...RAW...IMAGE...BITS...
--7MA4YWxkTrZu0gW--

This was the only way to send files for a long time, so many servers expect data in this format. The multipartBody function lets you create these requests. For example, to upload a photo album all at once, you could create a body like this:

multipartBody
  [ stringPart "title" "Trip to London"
  , filePart "album[]" file1
  , filePart "album[]" file2
  , filePart "album[]" file3
  ]

All of the body parts need to have a name. Names can be repeated. Adding the [] on repeated names is a convention from PHP. It seems weird, but I see it enough to mention it. You do not have to do it that way, especially if your server uses some other convention!

The Content-Type: multipart/form-data header is automatically set when creating a body this way.

Note: Use track to track upload progress.

type Part

One part of a multipartBody.

stringPart : String -> String -> Part

A part that contains String data.

multipartBody
  [ stringPart "title" "Tom"
  , filePart "photo" tomPng
  ]
filePart : String -> File -> Part

A part that contains a file. You can use gren-lang/file to get files loaded into the browser. From there, you can send it along to a server like this:

multipartBody
  [ stringPart "product" "Ikea Bekant"
  , stringPart "description" "Great desk for home office."
  , filePart "image[]" file1
  , filePart "image[]" file2
  , filePart "image[]" file3
  ]
bytesPart : String -> String -> Bytes -> Part

A part that contains bytes, allowing you to use gren-lang/bytes to encode data exactly in the format you need.

multipartBody
  [ stringPart "title" "Tom"
  , bytesPart "photo" "image/png" bytes
  ]

Note: You must provide a MIME type so that the receiver has clues about how to interpret the bytes.

Expect

type Expect msg

Logic for interpreting a response body.

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

Expect the response body to be a String. Like when getting the full text of a book:

import Http

type Msg
  = GotText (Result Http.Error String)

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

The response body is always some sequence of bytes, but in this case, we expect it to be UTF-8 encoded text that can be turned into a String.

expectJson : (Result Error a -> msg) -> Decoder a -> Expect msg

Expect the response body to be JSON. Like if you want to get a random cat GIF you might say:

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

type Msg
  = GotGif (Result Http.Error String)

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

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

The official guide goes through this particular example here. That page also introduces gren-lang/json to help you get started turning JSON into Gren values in other situations.

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

expectBytes : (Result Error a -> msg) -> Decoder a -> Expect msg

Expect the response body to be binary data. For example, maybe you are talking to an endpoint that gives back ProtoBuf data:

import Bytes.Decode as Bytes
import Http

type Msg
  = GotData (Result Http.Error Data)

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

-- dataDecoder : Bytes.Decoder Data

You would use gren-lang/bytes to decode the binary data according to a proto definition file like example.proto.

If the decoder fails, you get a BadBody error that just indicates that something went wrong. It probably makes sense to debug by peeking at the bytes you are getting in the browser developer tools or something.

expectWhatever : (Result Error {} -> msg) -> Expect msg

Expect the response body to be whatever. It does not matter. Ignore it! For example, you might want this when uploading files:

import Http

type Msg
  = Uploaded (Result Http.Error {})

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

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

type Error
= BadUrl String
| Timeout
| NetworkError
| BadStatus Int
| BadBody String

A Request can fail in a couple ways:

  • BadUrl means you did not provide a valid URL.
  • Timeout means it took too long to get a response.
  • NetworkError means the user turned off their wifi, went in a cave, etc.
  • BadStatus means you got a response back, but the status code indicates failure.
  • BadBody means you got a response back with a nice status code, but the body of the response was something unexpected. The String in this case is a debugging message that explains what went wrong with your JSON decoder or whatever.

Note: You can use expectStringResponse and expectBytesResponse to get more flexibility on this.

Progress

track : String -> (Progress -> msg) -> Sub msg

Track the progress of a request. Create a request where tracker = Just "form.pdf" and you can track it with a subscription like track "form.pdf" GotProgress.

type Progress
= Sending ({ sent : Int, size : Int })
| Receiving ({ received : Int, size : Maybe Int })

There are two phases to HTTP requests. First you send a bunch of data, then you receive a bunch of data. For example, say you use fileBody to upload a file of 262144 bytes. From there, progress will go like this:

Sending   { sent =      0, size = 262144 }  -- 0.0
Sending   { sent =  65536, size = 262144 }  -- 0.25
Sending   { sent = 131072, size = 262144 }  -- 0.5
Sending   { sent = 196608, size = 262144 }  -- 0.75
Sending   { sent = 262144, size = 262144 }  -- 1.0
Receiving { received =  0, size = Just 16 } -- 0.0
Receiving { received = 16, size = Just 16 } -- 1.0

With file uploads, the send phase is expensive. That is what we saw in our example. But with file downloads, the receive phase is expensive.

Use fractionSent and fractionReceived to turn this progress information into specific fractions!

Note: The size of the response is based on the Content-Length header, and in rare and annoying cases, a server may not include that header. That is why the size is a Maybe Int in Receiving.

fractionSent : { sent : Int, size : Int } -> Float

Turn Sending progress into a useful fraction.

fractionSent { sent =   0, size = 1024 } == 0.0
fractionSent { sent = 256, size = 1024 } == 0.25
fractionSent { sent = 512, size = 1024 } == 0.5

-- fractionSent { sent = 0, size = 0 } == 1.0

The result is always between 0.0 and 1.0, ensuring that any progress bar animations never go out of bounds.

And notice that size can be zero. That means you are sending a request with an empty body. Very common! When size is zero, the result is always 1.0.

Note: If you create your own function to compute this fraction, watch out for divide-by-zero errors!

fractionReceived : { received : Int, size : Maybe Int } -> Float

Turn Receiving progress into a useful fraction for progress bars.

fractionReceived { received =   0, size = Just 1024 } == 0.0
fractionReceived { received = 256, size = Just 1024 } == 0.25
fractionReceived { received = 512, size = Just 1024 } == 0.5

-- fractionReceived { received =   0, size = Nothing } == 0.0
-- fractionReceived { received = 256, size = Nothing } == 0.0
-- fractionReceived { received = 512, size = Nothing } == 0.0

The size here is based on the Content-Length header which may be missing in some cases. A server may be misconfigured or it may be streaming data and not actually know the final size. Whatever the case, this function will always give 0.0 when the final size is unknown.

Furthermore, the Content-Length header may be incorrect! The implementation clamps the fraction between 0.0 and 1.0, so you will just get 1.0 if you ever receive more bytes than promised.

Note: If you are streaming something, you can write a custom version of this function that just tracks bytes received. Maybe you show that 22kb or 83kb have been downloaded, without a specific fraction. If you do this, be wary of divide-by-zero errors because size can always be zero!

Cancel

cancel : String -> Cmd msg

Try to cancel an ongoing request based on a tracker.

Risky Requests

riskyRequest :
{ method : String
, headers : Array Header
, url : String
, body : Body
, expect : Expect msg
, timeout : Maybe Float
, tracker : Maybe String
}
-> Cmd msg

Create a request with a risky security policy. Things like:

  • Allow responses from other domains to set cookies.
  • Include cookies in requests to other domains.

This is called withCredentials in JavaScript, and it allows a couple other risky things as well. It can be useful if www.example.com needs to talk to uploads.example.com, but it should be used very carefully!

For example, every HTTP request includes a Origin header revealing the domain, so any request to facebook.com reveals the website that sent it. From there, cookies can be used to correlate browsing habits with specific users. “Oh, it looks like they visited example.com. Maybe they want ads about examples!” This is why you can get shoe ads for months without saying anything about it on any social networks. This risk exists even for people who do not have an account. Servers can set a new cookie to uniquely identify the browser and build a profile around that. Same kind of tricks for logged out users.

Context: A significantly worse version of this can happen when trying to add integrations with Google, Facebook, Pinterest, Twitter, etc. “Add our share button. It is super easy. Just add this <script> tag!” But the goal here is to get arbitrary access to the executing context. Now they can track clicks, read page content, use time zones to approximate location, etc. As of this writing, suggesting that developers just embed <script> tags is the default for Google Analytics, Facebook Like Buttons, Twitter Follow Buttons, Pinterest Save Buttons, and Instagram Embeds.

Elaborate Expectations

expectStringResponse :
(Result x a -> msg)
-> (Response String -> Result x a)
-> Expect msg

Expect a Response with a String body. So you could define your own expectJson like this:

import Http
import Json.Decode as D

expectJson : (Result Http.Error a -> msg) -> D.Decoder a -> Expect msg
expectJson toMsg decoder =
  expectStringResponse toMsg <|
    \response ->
      case response of
        Http.BadUrl_ url ->
          Err (Http.BadUrl url)

        Http.Timeout_ ->
          Err Http.Timeout

        Http.NetworkError_ ->
          Err Http.NetworkError

        Http.BadStatus_ metadata body ->
          Err (Http.BadStatus metadata.statusCode)

        Http.GoodStatus_ metadata body ->
          case D.decodeString decoder body of
            Ok value ->
              Ok value

            Err err ->
              Err (Http.BadBody (D.errorToString err))

This function is great for fancier error handling and getting response headers. For example, maybe when your sever gives a 404 status code (not found) it also provides a helpful JSON message in the response body. This function lets you add logic to the BadStatus_ branch so you can parse that JSON and give users a more helpful message! Or make your own custom error type for your particular application!

expectBytesResponse :
(Result x a -> msg)
-> (Response Bytes -> Result x a)
-> Expect msg

Expect a Response with a Bytes body.

It works just like expectStringResponse, giving you more access to headers and more leeway in defining your own errors.

type Response body
= BadUrl_ String
| Timeout_
| NetworkError_
| BadStatus_ Metadata body
| GoodStatus_ Metadata body

A Response can come back a couple different ways:

  • BadUrl_ — you did not provide a valid URL.
  • Timeout_ — it took too long to get a response.
  • NetworkError_ — the user turned off their wifi, went in a cave, etc.
  • BadStatus_ — a response arrived, but the status code indicates failure.
  • GoodStatus_ — a response arrived with a nice status code!

The type of the body depends on whether you use expectStringResponse or expectBytesResponse.

type alias Metadata =
{ url : String
, statusCode : Int
, statusText : String
, headers : Dict String String
}

Extra information about the response:

  • 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

Note: It is possible for a response to have the same header multiple times. In that case, all the values end up in a single entry in the headers dictionary. The values are separated by commas, following the rules outlined here.

Tasks

task :
{ method : String
, headers : Array Header
, url : String
, body : Body
, resolver : Resolver x a
, timeout : Maybe Float
}
-> Task x a

Just like request, but it creates a Task. This makes it possible to pair your HTTP request with Time.now if you need timestamps for some reason. This should be quite rare.

type Resolver x a

Describes how to resolve an HTTP task. You can create a resolver with stringResolver and bytesResolver.

stringResolver : (Response String -> Result x a) -> Resolver x a

Turn a response with a String body into a result. Similar to expectStringResponse.

bytesResolver : (Response Bytes -> Result x a) -> Resolver x a

Turn a response with a Bytes body into a result. Similar to expectBytesResponse.

riskyTask :
{ method : String
, headers : Array Header
, url : String
, body : Body
, resolver : Resolver x a
, timeout : Maybe Float
}
-> Task x a

Just like riskyRequest, but it creates a Task. Use with caution! This has all the same security concerns as riskyRequest.