HTTP client¶
Spindle_client calls other HTTP and HTTPS servers. It is its own library,
so a program that only calls out links no framework:
Making a request¶
let () =
Eio_main.run @@ fun env ->
Spindle_client.run env @@ fun client ->
match Spindle_client.call client `GET "https://example.com/" with
| Ok { status; body; _ } ->
Printf.printf "%d, %d bytes\n"
(Spindle_http.Status.to_int status)
(String.length body)
| Error e -> prerr_endline (Spindle_client.error_to_string e)
Spindle_client.run env f makes a client for as long as f runs and closes
its connections when f returns.
call client meth url answers Ok with the status (the same
Spindle_http.Status.t variant your routes answer with), the headers (names
lower-cased) and the whole body – any status is an answer, so a
`Not_found is Ok. Error says whether the request went out:
`Unsent (no connection, bad TLS, a URL it cannot call – the server never
saw it, so sending it again is safe) or `Lost (it may have gone, and the
answer was cut short, unreadable, or never came before the deadline – the
server may have acted on it). Send a
body with ~body and ~headers:
match
Spindle_client.call client ~timeout:Mtime.Span.(10 * s)
~headers:[ ("content-type", "application/json") ] ~body
`POST "https://api.example.com/v1/things"
with
| Ok { status = `OK; body; _ } -> decode body
| Ok r ->
Error
(Printf.sprintf "upstream answered %d" (Spindle_http.Status.to_int r.status))
| Error e -> Error (Spindle_client.error_to_string e)
Using a client inside a server¶
Make it once, with Spindle_client.create, on the application’s switch –
never a request’s, since the connections it keeps outlive any one request –
and supply it to the routes as a
resource, which they read with
let+ client = upstream:
let upstream : Spindle_client.t Spindle.Dep.t =
Spindle.Dep.of_resource "upstream"
let () =
Eio_main.run @@ fun env ->
Eio.Switch.run @@ fun sw ->
let client =
Spindle_client.create ~sw ~net:(Eio.Stdenv.net env)
~mono_clock:(Eio.Stdenv.mono_clock env) ()
in
Spindle.serve env ~resources:[ Spindle.Dep.resource upstream client ] routes
create and run take the same options:
| Option | Default | |
|---|---|---|
~timeout |
30 s | each call’s deadline, over the whole call; a call can name its own |
~max_body |
16 MiB | the longest answer read |
~per_host |
4 | idle connections kept per scheme, host and port, on each domain |
~idle_timeout |
15 s | how long an unused connection is kept |
~authenticator |
system trust store | what a server’s certificate is checked against |
Each wait is an Mtime.Span.t: ~timeout:Mtime.Span.(10 * s).
Good to know:
- Connections are reused when the answer was read to its end and neither side
said
close. A call never waits for a free connection; it opens its own. Limiting how many calls run at once is up to you. - A request is retried only when that is safe: one that could not be written
on a reused connection, or an idempotent one (
GET,HEAD,PUT,DELETE,OPTIONS,TRACE) whose reused connection closed before any answer. APOSTthat went out is never sent twice; it fails with`Lost, and you decide. - Each call logs one
debugline onspindle.client, never its headers or body.
Streaming a response¶
Spindle_client.stream hands your function the status, headers and a
body to read piece by piece – a streamed model answer, or a download too
large to hold:
Spindle_client.stream client `GET url (fun answer ->
let rec copy () =
match Spindle_client.Body.read answer.body with
| Ok (`Data piece) -> print_string piece; copy ()
| Ok `End -> Ok ()
| Error e -> Error e
in
copy ())
~timeout bounds the head, and ~read_timeout each wait for more of the
body. A read that fails is the body’s end: every read after it answers the
same error, so waiting longer means a longer ~read_timeout, not a second
read. To stop early, return: the connection is then closed.
Server-Sent Events¶
Spindle_client.events client url f asks for text/event-stream and hands
each event (its name, data, id and retry_ms) to f, which answers
`Continue or `Stop. It answers Finished (the server ended it) or
Stopped, each with the last event id, or Answered with the answer when it
was not an event stream. To reconnect, call it again with
~last_event_id.
WebSockets and tracing¶
Spindle_client.websocketopens a WebSocket: see WebSockets.- A call made while a request is traced is a span of its trace, and
Spindle_client.Otlpsends spans to a collector: see Tracing.