March Docs

HttpClient

HttpClient: high-level composable HTTP client with step pipeline.

This is Layer 3 of March's HTTP library. It provides a Req-style three-phase pipeline (request steps, response steps, error steps) built on top of HttpTransport.

Usage: let client = HttpClient.new_client() |> HttpClient.add_request_step("auth", HttpClient.Steps.bearer_auth("my-token")) |> HttpClient.with_redirects(5) let resp = HttpClient.get(client, "http://api.example.com/users")

Types

typeTransportErrorTransportError =#

TransportError mirrors HttpTransport.TransportError (no imports in March).

typeHttpErrorHttpError =#
typeRequestStepEntryRequestStepEntry = RequestStepEntry(String, a)#

Step entry types: wrap a name + function

typeResponseStepEntryResponseStepEntry = ResponseStepEntry(String, a)#
typeErrorRecoveryErrorRecovery = Recover(a) | Fail(HttpError)#
typeErrorStepEntryErrorStepEntry = ErrorStepEntry(String, a)#
typeClientClient = Client(#

Client: holds step lists and pipeline behavior config

Functions

fnadd_error_stepadd_error_step(client, name, step)#

Append a named error step. An error step is fn (req, err) -> ErrorRecovery: return Recover(resp) to turn the error into a successful response, or Fail(err2) to pass a (possibly new) error on to the next error step.

Examples

march> HttpClient.list_steps(HttpClient.add_error_step(HttpClient.new_client(), "fallback", fn (_, e) -> HttpClient.Fail(e)))
["error:fallback"]
fnadd_request_stepadd_request_step(client, name, step)#

Append a named request step. A request step is fn req -> Result(req, HttpError); steps run in the order they were added, before the request is sent. Names are only labels (see list_steps); duplicates are allowed.

Examples

march> HttpClient.list_steps(HttpClient.add_request_step(HttpClient.new_client(), "auth", HttpClient.step_bearer_auth("t")))
["request:auth"]
fnadd_response_stepadd_response_step(client, name, step)#

Append a named response step. A response step is fn (req, resp) -> Result((req, resp), HttpError); steps run in order after the response (and any redirects) arrive.

Examples

march> HttpClient.list_steps(HttpClient.add_response_step(HttpClient.new_client(), "check", HttpClient.step_raise_on_error))
["response:check"]
fnappend_to_listappend_to_list(xs, x)#

Append x to the end of xs (O(n)); used to keep steps in registration order.

Examples

march> HttpClient.append_to_list([1, 2], 3)
[1, 2, 3]
fndeletedelete(client, url)#

Build a DELETE request for url (empty String body) and run it. An unparseable URL gives Err(StepError("url", ...)).

Examples

march> HttpClient.delete(HttpClient.new_client(), "bad")
Err(StepError("url", "invalid url: bad"))
fngetget(client, url)#

Build a GET request for url (empty String body) and run it through the client. An unparseable URL gives Err(StepError("url", "invalid url: ...")) without running any step.

Examples

HttpClient.get(HttpClient.new_client(), "http://api.example.com/users")

march> HttpClient.get(HttpClient.new_client(), "not a url")
Err(StepError("url", "invalid url: not a url"))
fnhandle_redirectshandle_redirects(req, resp, max, count)#

Follow redirects for resp, the answer to req. Returns Ok(resp) unchanged when max == 0, when resp is not a 3xx, or when it has no Location header; Err(TooManyRedirects(count)) once count >= max. Otherwise re-issues the request as a body-less GET (whatever the original method and redirect code) over a new connection and recurses. An absolute Location that crosses origins drops credential headers; a relative one keeps all headers and replaces only the path.

Examples

march> let req = Result.unwrap(Http.get("http://example.com/users?page=2"))
march> let sreq = Http.set_body(req, "")
march> HttpClient.handle_redirects(sreq, Http.Response(Http.Status(302), [Http.Header("Location", "/x")], ""), 2, 2)
Err(TooManyRedirects(2))
march> let req = Result.unwrap(Http.get("http://example.com/users?page=2"))
march> let sreq = Http.set_body(req, "")
march> HttpClient.handle_redirects(sreq, Http.Response(Http.Status(302), [], ""), 3, 0) |> Result.map(fn r -> Http.response_status_code(r))
Ok(302)
fnhc_effective_porthc_effective_port(sch, port_opt)#

The port a request actually uses: the explicit port, or the scheme default (80 for http, 443 for https).

Examples

march> HttpClient.hc_effective_port(Http.SchemeHttps, None)
443
march> HttpClient.hc_effective_port(Http.SchemeHttp, Some(8080))
8080
fnhc_is_sensitive_header_namehc_is_sensitive_header_name(name)#

True for header names that carry credentials and are dropped on a cross-origin redirect: Authorization, Cookie, Proxy-Authorization (case-insensitive).

Examples

march> HttpClient.hc_is_sensitive_header_name("COOKIE")
true
march> HttpClient.hc_is_sensitive_header_name("Accept")
false
fnhc_same_originhc_same_origin(req_a, req_b)#

True when two requests share an origin: same scheme, same host (case-insensitive) and same effective port.

Examples

march> HttpClient.hc_same_origin(Result.unwrap(Http.get("http://Example.com/a")), Result.unwrap(Http.get("http://example.com:80/b")))
true
march> HttpClient.hc_same_origin(Result.unwrap(Http.get("http://example.com/a")), Result.unwrap(Http.get("https://example.com/b")))
false
fnhc_strip_sensitive_headershc_strip_sensitive_headers(headers)#

Remove every credential-bearing header (see hc_is_sensitive_header_name) from a header list, keeping the order of the rest.

Examples

march> HttpClient.hc_strip_sensitive_headers([Http.Header("Cookie", "a"), Http.Header("Accept", "*/*")])
[Header("Accept", "*/*")]
fnlist_stepslist_steps(client)#

List the client's step names in execution order, each prefixed by its phase: "request:", then "response:", then "error:".

Examples

march> HttpClient.new_client() |> HttpClient.add_error_step("e", fn (_, e) -> HttpClient.Fail(e)) |> HttpClient.add_request_step("a", HttpClient.step_default_headers) |> HttpClient.list_steps
["request:a", "error:e"]
fnnew_clientnew_client()#

Create a new bare client with no steps and no redirect/retry.

Examples

march> HttpClient.list_steps(HttpClient.new_client())
[]
fnpostpost(client, url, bdy)#

Build a POST request for url with body bdy and run it. An unparseable URL gives Err(StepError("url", ...)).

Examples

let client = HttpClient.new_client()
  |> HttpClient.add_request_step("json", HttpClient.step_content_type("application/json"))
HttpClient.post(client, "http://api.example.com/users", json_body)

march> HttpClient.post(HttpClient.new_client(), "ftp://x", "body")
Err(StepError("url", "invalid url: ftp://x"))
fnput_requestput_request(client, url, bdy)#

Build a PUT request for url with body bdy and run it. An unparseable URL gives Err(StepError("url", ...)).

Examples

HttpClient.put_request(client, "http://api.example.com/users/1", body)
fnreconnect_and_retryreconnect_and_retry(req, retries_left)#

Open a new connection for req and continue transport_keepalive on it with retries_left - 1. Returns the connect error if that fails.

fnrunrun(client, req)#

Run a request through the client's full step pipeline:

  1. Run request steps left-to-right; the first Err short-circuits.
  2. Send with HttpTransport.request (a new connection per attempt),
retrying up to `max_retries` extra times on transport errors
(the configured backoff is currently not applied between attempts).
  1. Follow redirects if with_redirects set a non-zero limit.
  2. Run response steps left-to-right.
  3. Any Err from steps 1-4 is fed to the error steps in order; the first
`Recover(resp)` turns it into `Ok(resp)`, otherwise the last error is
returned. Transport errors arrive wrapped in `HttpTransportError`.

Examples

let req = Result.unwrap(Http.get("http://api.example.com/users"))
match HttpClient.run(client, Http.set_body(req, "")) do
  Ok(resp) -> Http.response_body(resp)
  Err(e) -> "request failed"
end
fnrun_error_stepsrun_error_steps(steps, req, err)#

Offer err to each ErrorStepEntry in order. The first Recover(resp) wins and gives Ok(resp); a Fail(e2) replaces the error for the next step. With no step recovering, returns Err of the last error.

Examples

march> let req = Result.unwrap(Http.get("http://example.com/users?page=2"))
march> HttpClient.run_error_steps([], req, HttpClient.TooManyRedirects(3))
Err(TooManyRedirects(3))
march> let req = Result.unwrap(Http.get("http://example.com/users?page=2"))
march> HttpClient.run_error_steps([HttpClient.ErrorStepEntry("x", fn (_, _) -> HttpClient.Recover(42))], req, HttpClient.TooManyRedirects(3))
Ok(42)
fnrun_on_fdrun_on_fd(fd, req_steps, resp_steps, err_steps, max_redir, max_retries, req)#

Run req through the pipeline on an already-open connection fd. This is the engine behind the do_request callback of with_connection.

Same phases as run, except that the request is sent with transport_keepalive(fd, req, max_retries + 1): on a send/receive failure the connection is closed and a fresh one opened. Redirects still go over new connections (handle_redirects). Returns Ok(response) or the result of the error steps.

fnrun_request_stepsrun_request_steps(steps, req)#

Thread req through a list of RequestStepEntry values left-to-right; stops at the first Err and returns it.

Examples

march> let req = Result.unwrap(Http.get("http://example.com/users?page=2"))
march> let ct = HttpClient.step_content_type("application/json")
march> HttpClient.run_request_steps([HttpClient.RequestStepEntry("ct", ct)], req) |> Result.map(fn r -> Http.get_request_header(r, "content-type"))
Ok(Some("application/json"))
fnrun_response_stepsrun_response_steps(steps, req, resp)#

Thread (req, resp) through a list of ResponseStepEntry values left-to-right; stops at the first Err and returns it.

Examples

march> let req = Result.unwrap(Http.get("http://example.com/users?page=2"))
march> HttpClient.run_response_steps([HttpClient.ResponseStepEntry("raise", HttpClient.step_raise_on_error)], req, Http.Response(Http.Status(500), [], ""))
Err(StepError("step_raise_on_error", "500"))
fnstep_base_urlstep_base_url(base)#

Returns a request step that re-targets a request at base ++ path: the scheme, host, port and path come from parsing that string, while the method, query, headers and body are kept from the request. Fails with StepError("step_base_url", ...) if the combined URL does not parse.

Examples

march> let req = Result.unwrap(Http.get("http://example.com/users?page=2"))
march> let base = HttpClient.step_base_url("https://api.example.com:8443/v1")
march> Result.map(base(req), fn r -> (Http.host(r), Http.port(r), Http.path(r), Http.query(r)))
Ok(("api.example.com", Some(8443), "/v1/users", Some("page=2")))
march> let req = Result.unwrap(Http.get("http://example.com/users?page=2"))
march> let bad = HttpClient.step_base_url("nope")
march> Result.is_err(bad(req))
true
fnstep_basic_authstep_basic_auth(user, pass)#

Returns a request step that sets Authorization: Basic <user>:<pass>.

Note: the credentials are sent as the literal user:pass text, NOT base64-encoded as RFC 7617 requires, so most servers will reject them.

Examples

march> let req = Result.unwrap(Http.get("http://example.com/users?page=2"))
march> let basic = HttpClient.step_basic_auth("ann", "pw")
march> Result.map(basic(req), fn r -> Http.get_request_header(r, "Authorization"))
Ok(Some("Basic ann:pw"))
fnstep_bearer_authstep_bearer_auth(token)#

Returns a request step that sets Authorization: Bearer <token>.

Examples

march> let req = Result.unwrap(Http.get("http://example.com/users?page=2"))
march> let auth = HttpClient.step_bearer_auth("tok")
march> Result.map(auth(req), fn r -> Http.get_request_header(r, "Authorization"))
Ok(Some("Bearer tok"))
fnstep_content_typestep_content_type(ct)#

Returns a request step that sets the Content-Type header to ct.

Examples

march> let req = Result.unwrap(Http.get("http://example.com/users?page=2"))
march> let ct = HttpClient.step_content_type("application/json")
march> Result.map(ct(req), fn r -> Http.get_request_header(r, "Content-Type"))
Ok(Some("application/json"))
fnstep_default_headersstep_default_headers(req)#

Request step: set User-Agent: march/0.1 and Accept: */*, replacing any existing values. Never fails. Register it with add_request_step(client, "defaults", HttpClient.step_default_headers).

Examples

march> let req = Result.unwrap(Http.get("http://example.com/users?page=2"))
march> Result.map(HttpClient.step_default_headers(req), fn r -> Http.get_request_header(r, "User-Agent"))
Ok(Some("march/0.1"))
fnstep_raise_on_errorstep_raise_on_error(req, resp)#

Response step: return Err(StepError("step_raise_on_error", "<code>")) for any status >= 400, otherwise pass (req, resp) through unchanged.

Examples

march> let req = Result.unwrap(Http.get("http://example.com/users?page=2"))
march> HttpClient.step_raise_on_error(req, Http.Response(Http.Status(404), [], ""))
Err(StepError("step_raise_on_error", "404"))
march> let req = Result.unwrap(Http.get("http://example.com/users?page=2"))
march> HttpClient.step_raise_on_error(req, Http.Response(Http.Status(204), [], "")) |> Result.is_ok
true
fnstream_getstream_get(client, url, on_chunk)#

Stream a GET response body. Calls on_chunk(chunk_string) for each chunk as it arrives over the wire. Returns Ok((status, headers, ())); the values returned by on_chunk are discarded (HttpTransport streams for side effects only).

Only the client's request steps are applied; response steps, error steps, redirects and retries are not. An invalid URL gives Err(StepError("url", ...)); a failing request step's Err is returned as is.

Examples

HttpClient.stream_get(client, "http://example.com/bigfile", fn chunk ->
  println("got " ++ int_to_string(string_length(chunk)) ++ " bytes"))

march> HttpClient.stream_get(HttpClient.new_client(), "bad", fn c -> c)
Err(StepError("url", "invalid url: bad"))
fntransport_keepalivetransport_keepalive(fd, req, retries_left)#

Send req on the open connection fd. Returns Ok((fd, resp)) on success. On failure, closes fd and, while retries_left > 0, reconnects and tries again (each reconnect consumes two from retries_left); otherwise returns Err(Closed), discarding the original transport error. After a reconnect the returned fd is the new one.

fntransport_with_retrytransport_with_retry(req, retries_left)#

Send req with HttpTransport.request (a fresh connection each time), retrying immediately up to retries_left more times on any transport error. Returns the last error if every attempt fails.

fnwith_connectionwith_connection(client, url, callback)#

Open a keep-alive connection and run a function with it. The function receives a do_request callback that sends requests on the shared connection. Connection is closed when done.

Returns Ok(result) where result is whatever the callback returned (each do_request(req) itself returns Result(Response, HttpError)), Err(StepError("url", ...)) if url does not parse, or Err(HttpTransportError(e)) if the connection cannot be opened. Only the scheme, host and port of url are used to connect; each request carries its own path. If the callback panics the connection is not closed.

Examples

HttpClient.with_connection(client, "http://api.example.com", fn do_request ->
  let users = Result.unwrap(Http.get("http://api.example.com/users"))
  let posts = Result.unwrap(Http.get("http://api.example.com/posts"))
  let r1 = do_request(Http.set_body(users, ""))
  let r2 = do_request(Http.set_body(posts, ""))
  (r1, r2))

march> HttpClient.with_connection(HttpClient.new_client(), "bad", fn d -> 1)
Err(StepError("url", "invalid url: bad"))
fnwith_redirectswith_redirects(client, max)#

Follow up to max redirects (0, the default, disables following). Every redirect is re-issued as a body-less GET; exceeding the limit yields TooManyRedirects(max).

Examples

march> HttpClient.with_redirects(HttpClient.new_client(), 5) == HttpClient.Client(Nil, Nil, Nil, 5, 0, 0)
true
fnwith_retrywith_retry(client, max_attempts, backoff_ms)#

Retry a failed transport call up to max_attempts extra times (0 disables). Only transport errors are retried, not HTTP error statuses. backoff_ms is stored but run currently retries immediately without sleeping.

Examples

march> HttpClient.with_retry(HttpClient.new_client(), 3, 100) == HttpClient.Client(Nil, Nil, Nil, 0, 3, 100)
true