DocsStandard library

HTTP client.

Send requests through an explicit transport handler and choose buffered or streaming responses.

Http.Client describes requests with an Http effect. Client.run connects that effect to the network, and it’s also the default handler, so a main that sends requests gets it without asking. Install it yourself when you want to choose how long its connection pool lives, or install a simulated runner for tests. Handle request failures at whatever boundary can report them or recover from them.

API reference: Http.Client

Running requests.

import Fail
import Http.Client as Client
import Result exposing (Result(..))

main() =
    result = Fail.attempt { Client.run { Client.getText "https://example.com/" } }
    case result of
        Ok text -> print text
        Err error -> print error

This program needs native network access. getText expects a successful 2xx response and decodes the body as text. Client.run performs its requests through Net.Network and owns a connection pool until its callback completes, and request errors still propagate to Fail.attempt.

Requests and responses.

You can build a request value first and decide how to consume the response later. This adds a bearer header without making a connection:

import Http.Client as Client

main() =
    outgoing = Client.withBearer "test-token" (Client.request "GET" "https://example.com/items")
    print outgoing.method
    print (List.length outgoing.headers)
GET
1

withHeader, withBearer, and withBody each return a customized request value. fetch sends a request and collects its body into a Reply, while send hands a scoped streaming response to a callback.

get, post, and fetch return whatever HTTP status the server sends. The data helpers and expectSuccess raise BadStatus for anything that isn’t 2xx. Before using a success-only helper, consider whether an error response is a normal part of your application’s protocol.

For large bodies, consume the response inside send. Its reader can’t escape the callback, but decoded data can. Buffered operations enforce maxBodyBytes, whereas a streaming callback has to set its own limits.

Configuration.

configure adjust action changes the settings for the requests nested inside it. With a record update to the configuration, you can set a base URL, extra headers, byte limits, or timeouts. Relative URLs need a configured base.

The defaults are a 10-second connection timeout, 30-second read and write waits, 64 KiB headers, 10 MiB buffered bodies, and ten redirects. The read and write timeouts bound each individual wait, not the whole request. HTTPS verifies certificates using the system roots, plus an optional extra PEM file.

The client looks after framing headers such as Content-Length itself, so don’t set them by hand.

Redirects.

The client follows supported redirects, up to the configured limit. A 303 normally turns into a GET, and a repeatable body can be resent when a redirect keeps the method. A streaming body can’t be replayed automatically. Credentials are stripped once a redirect leaves the original origin, and a redirect from HTTPS to another scheme is returned to you rather than followed.

Compression.

Response decompression is on by default and supports gzip. If you set Accept-Encoding yourself, you get the encoding you asked for, without the automatic decoding. Buffered limits count decompressed bytes.

compressRequest gzip-encodes an outgoing body on request. Only use it when the server accepts compressed requests.

Helpers.

getBytes collects binary data, and getJson @Type decodes typed JSON. postJson encodes a value and decodes a typed response. JSON explains witnesses and derived codecs.

To exercise client code without sockets, give stub a serialized response:

import Bytes
import Fail
import Http.Client as Client
import Result exposing (Result(..))

reply _ = Ok (Bytes.fromString "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello")

main() =
    print (Fail.attempt {
        Client.stub reply { Client.getText "http://example.test/" }
    })
Ok "hello"

reply receives the serialized request bytes and returns response bytes or a client error, and the usual parsing and response checks still run. The stub can use the caller’s effects to record requests, which makes it handy for testing headers, paths, and recovery without a live service.

Testing with a server handler.

If you already have a server handler, mock lets your client talk to it in memory:

import Bytes
import Fail
import Http exposing (Body(..))
import Http.Client as Client
import Http.Server as Server

hello : Server.Request e ->{e} Server.Response e
hello _ = Server.Response { status = 200, headers = [], body = BytesBody (Bytes.fromString "hello") }

main() =
    print (Fail.attempt {
        use Client.mock hello
        Client.getText "http://example.test/"
    })
Ok "hello"

No socket is opened, and mock doesn’t need Net.Network at all. The client still writes its normal HTTP bytes, the server parses them and serializes the handler’s response, and the client parses that in turn. Framing and body handling get exercised on both sides. The handler is scoped, so it can read the request body or return a response that streams from it, but the request’s private permission can’t escape.

Each exchange is buffered in full. Tasks that inherit the mock can send requests at the same time, each in its own exchange, and the handler runs in the requesting task. If the server side hits a protocol error, you get the same error response or truncated body a real server would send, while failures in your own handler still propagate. Timing, TLS verification, and connection pooling aren’t simulated.

Use mock when your test is built around a server handler, and keep stub for tests that need to supply raw response bytes. Both fit inside an ordinary test body.

Cancellation.

Inside Client.cancellable, cancelling a task that’s blocked reading or writing a request closes that connection, and the task stops as cancelled instead of failing with a network error. Connecting, including a TLS handshake, isn’t interrupted. stub and mock never block, so they don’t need it.

Errors.

A failure can be an invalid URL, a transport error, a timeout, a protocol error, too many redirects, an unsuccessful status, or an invalid JSON body. Use Fail.attempt to get a result, and take care not to mix up a network failure with a server’s ordinary error status.