APIHttp.Client

Http.Client

An HTTP/1.1 client.

Code that makes requests performs the Http effect and Fail Error, and performs no networking itself: run connects Http to Net where the program starts it. stub answers raw bytes and mock runs server handlers in memory, for tests. Requests follow redirects, decompress gzip responses, reuse connections, and support https. A URL without a scheme resolves against the configured base URL.

Config

type Config =    { baseUrl : Maybe Url    , headers : List Header    , userAgent : String    , decompress : Bool    , connectTimeoutMs : Int    , readTimeoutMs : Int    , writeTimeoutMs : Int    , maxHeaderBytes : Int    , maxBodyBytes : Int    , maxRedirects : Int    , caFile : Maybe String    }Source

How requests are sent. configure adjusts it for a part of the program.

Fields

baseUrl

baseUrl : Maybe UrlSource

What a URL without a scheme resolves against.

headers

headers : List HeaderSource

Headers sent with every request; a request’s own replace them.

userAgent

userAgent : StringSource

The User-Agent header. It and, with decompress, Accept-Encoding: gzip come first; headers replace them, and a request’s own headers replace both.

decompress

decompress : BoolSource

Whether to ask for gzip responses and decode them as they stream. HEAD, 204, and 304 responses and other content codings are left alone, and a configuration or request that sets Accept-Encoding itself gets the body as sent.

connectTimeoutMs

connectTimeoutMs : IntSource

How long connecting, including a TLS handshake, may take, in milliseconds.

readTimeoutMs

readTimeoutMs : IntSource

How long each wait to read may take, in milliseconds.

writeTimeoutMs

writeTimeoutMs : IntSource

How long each wait to write may take, in milliseconds.

maxHeaderBytes

maxHeaderBytes : IntSource

The largest status line and header block, in bytes.

maxBodyBytes

maxBodyBytes : IntSource

The largest body read into memory, in decoded bytes. A send callback reading the stream itself has no limit.

maxRedirects

maxRedirects : IntSource

How many redirects to follow; zero returns redirects as they are.

caFile

caFile : Maybe StringSource

A PEM file of certificate authorities trusted besides the system’s. https sends the host for SNI and verifies its certificate against both.

Connection

type ConnectionSource

A transport’s handle on one connection: a Net connection owned by the transport’s runner, and whether it was reused from a pool.

Error

type Error    = InvalidUrl String    | Transport Error    | Timeout    | Protocol Error    | TooManyRedirects    | BadStatus Reply    | BadBody ErrorSource

What can go wrong with a request. Only the helpers that read a body as data raise BadStatus and BadBody.

Constructors

InvalidUrl

InvalidUrl : String -> ErrorSource

The URL does not parse, has no host, carries user information, uses a scheme other than http or https, or is relative with no base URL.

Transport

Transport : Error -> ErrorSource

Connecting, reading, or writing failed, including TLS verification.

Timeout

Timeout : ErrorSource

A connect, read, or write wait exceeded its timeout. It is never reported as a Transport failure of kind TimedOut.

Protocol

Protocol : Error -> ErrorSource

The exchange broke HTTP framing, in either direction.

TooManyRedirects

TooManyRedirects : ErrorSource

More redirects than maxRedirects.

BadStatus

BadStatus : Reply -> ErrorSource

A body helper got a status other than 2xx.

BadBody

BadBody : Error -> ErrorSource

A JSON helper could not decode the body.

Instances:

Eq Error
Show Error

Reply

type Reply =    { url : Url    , status : Int    , headers : List Header    , body : Bytes    }Source

A response with its body read into memory.

Fields

url

url : UrlSource

Where the response came from, after any redirects.

status

status : IntSource

The status code.

headers

headers : List HeaderSource

The header fields.

body

body : BytesSource

The whole body.

Instances:

Eq Reply
Show Reply

Request

type Request e =    { method : String    , url : String    , headers : List Header    , body : Body e    }Source

A request whose body writes perform e. Build one with request.

Fields

method

method : StringSource

The method, such as "GET".

url

url : StringSource

The URL, parsed when the request is sent.

headers

headers : List HeaderSource

The request’s own headers, which replace same-named configured ones. Setting a client-managed header, here or in Config.headers, fails with Protocol (InvalidMessage …). Host, Content-Length, Transfer-Encoding, and Connection are the client’s to write.

body

body : Body eSource

The body. An Empty body sends no framing headers for GET, HEAD, OPTIONS, TRACE, and DELETE, and Content-Length: 0 otherwise. It runs between the client’s writes, with the caller’s effects, so a large payload is produced while it is sent.

Response

type Response e =    { url : Url    , status : Int    , headers : List Header    , body : Reader e    }Source

A response whose body streams inside send’s callback.

Fields

url

url : UrlSource

Where the response came from, after any redirects.

status

status : IntSource

The status code.

headers

headers : List HeaderSource

The header fields; a decompressed body loses its Content-Encoding and Content-Length.

body

body : Reader eSource

The body, readable only while the callback runs.

Http

effect HttpSource

Sending requests over some transport: run for sockets, stub for raw response bytes, and mock for server handlers in memory. A main that performs it runs under run unless it installs another runner.

cancellable

cancellable : (() ->{Http, e} a) ->{Async, Http, e} aSource

Runs action so that cancelling a task blocked reading or writing a request interrupts it: the runner closes that connection and the task stops as cancelled rather than failing with the interrupted request’s error. Each task that inherits the wrapper is interrupted by its own cancellation. Connecting, including a TLS handshake, is not interrupted, and stub and mock never block.

compressRequest

compressRequest : Request e -> Request eSource

Sends the body gzip-compressed, which few servers accept unasked.

configure

configure : (Config -> Config) -> (() ->{Http, e} a) ->{Http, e} aSource

Runs action with the configuration adjusted. Overrides nest, and apply under every transport.

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

ok : Bytes -> Result Client.Error Bytes
ok _ = Ok (Bytes.fromString "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\n42")

agent =
    Fail.attempt {
        Client.stub ok { Client.configure { config -> { config | userAgent = "test" } } { (Client.get "http://h/").status } }
    }

agent == Ok 200

defaultConfig

defaultConfig : ConfigSource

No base URL or headers, the user agent fango, decompression on, a 10 s connect timeout, 30 s read and write timeouts, 64 KiB of headers, 10 MiB of body, up to 10 redirects, and no extra certificate authorities.

expectSuccess

expectSuccess : Reply ->{Fail Error} ReplySource

Fails with BadStatus unless the status is 2xx.

fetch

fetch : Request {Fail Error, Http, e} ->{Fail Error, Http, e} ReplySource

Sends request and reads the whole body, failing with Protocol BodyTooLarge beyond maxBodyBytes. Any status is a reply.

get

get : String ->{Fail Error, Http} ReplySource

Sends a GET and reads the whole body; any status is a reply.

getBytes

getBytes : String ->{Fail Error, Http} BytesSource

Sends a GET and answers the body of a 2xx reply, failing with BadStatus otherwise.

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

ok : Bytes -> Result Client.Error Bytes
ok _ = Ok (Bytes.fromString "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\n42")

Fail.attempt { Client.stub ok { Client.getBytes "http://h/" } } == Ok (Bytes.fromString "42")

getJson

getJson : Decode a => Type a -> String ->{Fail Error, Http} aSource

Sends a GET with Accept: application/json, unless the request sets it, and decodes a 2xx body within maxBodyBytes as it streams, failing with BadStatus for another status and BadBody for a body that does not decode.

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

ok : Bytes -> Result Client.Error Bytes
ok _ = Ok (Bytes.fromString "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\n42")

Fail.attempt { Client.stub ok { Client.getJson @Int "http://h/" } } == Ok 42

getText

getText : String ->{Fail Error, Http} StringSource

getBytes as text, with U+FFFD in place of malformed UTF-8.

mock

mock : (Request {Protocol, #s} ->{Protocol, #s} Response {Protocol, #s}) -> (() ->{Http, e} a) ->{e} aSource

Runs action against a server handler without sockets or Network. The client writes its real HTTP bytes, the server parses them and serializes the handler’s response, and the client reads that response normally. Requests and responses are buffered in full. Tasks that inherit the mock may send requests at the same time, each in its own exchange, and the handler runs in the requesting task, as under stub. Uses the server’s default framing limits and defaultConfig, which configure adjusts for client requests.

The handler may read the request body or return a body that streams from it, and use the caller’s effects to record requests. Server Protocol failures get the same error responses or truncated bodies as serve. Application failures propagate, and the caller handles Fail Error. Timing, TLS verification, and connection pooling are not simulated.

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

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

greeting =
    use Fail.attempt
    use Client.mock hello
    Client.getText "http://example.com/"

greeting == Ok "hello"

post

post : String -> Body {Fail Error, Http, e} ->{Fail Error, Http, e} ReplySource

Sends a POST with body and reads the whole response body; any status is a reply.

postJson

postJson : (Encode b, Decode a) => Type a -> String -> b ->{Fail Error, Http} aSource

Sends value as a chunked JSON POST with Content-Type: application/json, streamed as it is encoded; an encoding failure is Protocol (InvalidMessage …). It decodes a 2xx JSON response like getJson.

request

request : String -> String -> Request eSource

A request with method to target and no headers or body yet.

run

run : (() ->{Http, e} a) ->{Network, e} aSource

Runs requests over Net’s TCP and TLS connections with defaultConfig, which configure adjusts. Idle connections are kept for reuse, up to 8 per endpoint for up to 60 s, until run returns; tasks that inherit the handler share them. A request that fails on a reused connection before any response arrives is retried once on a new one when it is idempotent and its body can be sent again. Once any response bytes arrive, failures propagate without retry, including an incomplete head or body. A connection is reused only when its response was read to the end, both sides spoke HTTP/1.1, neither sent Connection: close, and the body was framed by length or chunks; an unread body is drained if at most 64 KiB remain, and otherwise the connection closes.

send

send : Request {Fail Error, Http, e} -> (Response {#s} ->{#s} a) ->{Fail Error, Http, e} aSource

Sends request and streams the response to action, which may read the body only while it runs; a value that does not depend on the reader can leave. Redirects (301, 302, 303, 307, 308) are followed first, up to maxRedirects. Location resolves against the current URL. A 303, or a 301 or 302 answering other than GET or HEAD, continues as a GET (a HEAD stays HEAD) without a body; other redirects repeat the method and body. Credentials are dropped once a redirect leaves the original origin, and a redirect without a usable Location, from https to another scheme, or needing a stream body again is returned as it is. Interim 1xx responses are skipped, 101 fails with Unsupported, HTTP/1.0 status lines are accepted, and a body that ends early fails with Malformed when read. A request body that fails partway closes the connection, and its failure reaches the caller unchanged. Any status is a normal response.

stub

stub : (Bytes ->{e} Result Error Bytes) -> (() ->{Http, e} a) ->{e} aSource

Answers each request without sockets: respond receives the request’s bytes exactly as written and answers a response’s bytes, or an Error to fail the request with. It may use the caller’s effects, for example to record requests. Requests start from defaultConfig.

Each connection is a Net connection of the stub’s own: one from another runner, or used after its request finished, fails with a Transport error. Tasks that inherit the stub may send requests at the same time; each gets its own exchange, and respond runs in the requesting task, so it may run concurrently with itself. Record requests with an atomic update, as under State.runAtomic, when tasks share the record.

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

ok : Bytes -> Result Client.Error Bytes
ok _ = Ok (Bytes.fromString "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\n42")

Fail.attempt { Client.stub ok { Client.getText "http://example.com/" } } == Ok "42"

withBearer

withBearer : String -> Request e -> Request eSource

The request with an Authorization: Bearer header for token.

withBody

withBody : Body e -> Request e -> Request eSource

The request with this body.

withHeader

withHeader : String -> String -> Request e -> Request eSource

The request with a header set, replacing any of the same name.

import Http exposing (Header(..))
import Http.Client as Client

(Client.withHeader "Accept" "text/plain" (Client.request "GET" "/x")).headers == [{ name = "Accept", value = "text/plain" }]

withResponse

withResponse : Url -> Bool -> Bool -> Int -> Reader {Fail Error, e} -> (Response {#s} ->{#s} a) ->{Fail Error, Protocol, e} aSource

Parses a response from input and scopes its body to action. A response to HEAD, and a 204 or 304, has no body; any other is framed by Content-Length, chunked encoding, or the connection closing. With decode, a gzip body is decompressed and loses the headers that described its encoded form.