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
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.
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.
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.
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
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.
Response
type Response e = { url : Url , status : Int , headers : List Header , body : Reader e }Source
A response whose body streams inside send’s callback.
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.
