http_gun

Starts HTTP clients on Gun and sends requests through them.

import gleam/http/request
import http_gun
import http_gun/config

pub fn main() {
  let assert Ok(client) = http_gun.start(config.default())
  let assert Ok(req) = request.to("https://example.com/data")
  let result = http_gun.send(client, request.set_body(req, <<>>))
  http_gun.stop(client)
  result
}

There is one function per operation: send collects a response into Buffered, open and with_response stream an owned body.Body, and batch runs many requests with bounded concurrency. Non-2xx statuses are response data. The client performs no retries, redirects or decompression.

Client views

Per-call settings live on the client handle. Each with_* view returns a new handle over the same pool, and every operation honours it:

let stream =
  client
  |> http_gun.with_timeout(config.Infinity)   // a long-lived SSE stream
  |> http_gun.with_correlation(order)

with_timeout and with_deadline replace the client’s request timeout, shorter or longer; the connect, pool and idle timeouts still apply. with_destination can only narrow the destinations the client admits. A client that serves several tenants admits the union of their destinations, which any call holding the handle without a view reaches; config.require_view_destination makes such a call fail closed with ViewDestinationRequired. Only a view policy that narrows the client’s destinations satisfies it; a policy that only tightens the plaintext rule does not, so a library tightening the scheme keeps the requirement.

A library that receives a view reads the caller’s correlation with correlation(client) and copies it into its own events, so one with_correlation by the caller tags both packages’ events.

Lifecycle

start links a client to the caller. Under a supervisor, use supervised(config, name); named(name) returns a handle that keeps working across restarts. During a restart, calls fail with ClientClosed and NotSent. stop refuses new work, lets open responses finish within the shutdown timeout, then cancels them.

http_gun/testing and http_gun/cassette start offline and recording clients of the same Client type.

Types

A collected response. Read fields by label; it may gain fields.

pub type Buffered {
  Buffered(
    response: response.Response(BitArray),
    trailers: List(#(String, String)),
    protocol: config.Negotiated,
    truncated: Bool,
  )
}

Constructors

  • Buffered(
      response: response.Response(BitArray),
      trailers: List(#(String, String)),
      protocol: config.Negotiated,
      truncated: Bool,
    )

    Arguments

    truncated

    True only under with_body_limit(_, _, Truncate) when the body exceeded its limit: the body holds the first limit bytes and the trailers are empty.

A shared client handle: the pool it sends through and the view settings of this handle. Copy it freely.

pub type Client =
  @internal Client
pub type Failure =
  error.Failure

The messages a supervised client’s process receives; name it with process.new_name for supervised and named.

pub type Message =
  @internal Message

What send does when a response body exceeds its limit.

pub type Overflow {
  Fail
  Truncate
}

Constructors

  • Fail

    Close the body and fail with LimitExceeded(ResponseBodyBytes, ..); error.status and error.headers return the response status and its headers, such as retry-after.

  • Truncate

    Close the body and return the status, headers and the first limit bytes, with Buffered.truncated set and no trailers.

Why a client did not start.

pub type StartError {
  InvalidConfig(config.ConfigError)
  InvalidScript(position: Int)
  ScriptTooLarge(limit: Int, observed: Int)
  StartFailed
}

Constructors

  • InvalidConfig(config.ConfigError)
  • InvalidScript(position: Int)

    The scripted exchange at this position has a status outside 200..599 or a body that is not whole bytes.

  • ScriptTooLarge(limit: Int, observed: Int)

    The script exceeds its 16 MiB budget.

  • StartFailed

    The Gun application or the client process failed to start.

Current pool counters. Read fields by label; it may gain fields.

pub type Stats {
  Stats(connections: Int, open_bodies: Int, queued_requests: Int)
}

Constructors

  • Stats(connections: Int, open_bodies: Int, queued_requests: Int)

Values

pub fn batch(
  client: Client,
  requests: List(request.Request(BitArray)),
  concurrency: Int,
) -> Result(List(Result(Buffered, error.Failure)), error.Failure)

Run at most concurrency requests at once, 1 to 1,024, over at most 10,000 requests. Results keep input order, and a failure occupies only its own position. Bodies retained across results are bounded by config.with_max_batch_bytes: once reached, the response being collected fails and requests not yet sent fail with LimitExceeded(BatchBytes, ..) and NotSent.

pub fn correlation(
  client: Client,
) -> option.Option(correlation.Correlation)

The correlation this view tags its events with, set by with_correlation, or None. A library that receives a caller’s view reads the caller’s correlation here and copies it into its own telemetry, instead of asking the caller to pass it twice:

let correlation = http_gun.correlation(client)
pub fn describe_start_error(problem: StartError) -> String
pub fn named(name: process.Name(Message)) -> Client

The handle of the client registered under name by supervised. Pure: it does not check that the client is running.

pub fn open(
  client: Client,
  req: request.Request(BitArray),
) -> Result(response.Response(body.Body), error.Failure)

Open a response whose body the calling process reads. Close the body when done, even after reading it to the end; prefer with_response when the response fits inside one callback.

pub fn send(
  client: Client,
  req: request.Request(BitArray),
) -> Result(Buffered, error.Failure)

Send a request and collect its response, up to the body limit. An oversized body fails with LimitExceeded(ResponseBodyBytes, ..), the response status and its headers, unless the view chose Truncate. Every failure after the head arrived keeps the status (error.status) and the headers after the client’s redaction (error.headers). The body is closed on every path.

pub fn start(
  settings: config.Config,
) -> Result(Client, StartError)

Validate the configuration, then start a client with its own connection pool, linked to the caller.

pub fn stats(client: Client) -> Result(Stats, error.Failure)

Read the pool’s counters. Holds no request history or credentials.

pub fn stop(client: Client) -> Nil

Refuse new requests, fail queued ones with ClientClosed and NotSent, let active HTTP exchanges finish within the shutdown timeout, then cancel them and close every connection. Stopping ends access to retained response bodies, including unread bytes after HTTP EOF. Callers needing old application results must await and consume them before stopping the client.

Returns when the client has stopped; stopping a stopped client returns at once. Meant for clients from start: a supervisor restarts a supervised client that stops.

pub fn supervised(
  settings: config.Config,
  name: process.Name(Message),
) -> supervision.ChildSpecification(Client)

A child specification for a supervisor. The client registers under name, so named(name) reaches it across restarts. An invalid configuration fails the child’s start.

let name = process.new_name("http_client")
let assert Ok(_) =
  static_supervisor.new(static_supervisor.OneForOne)
  |> static_supervisor.add(http_gun.supervised(config.default(), name))
  |> static_supervisor.start
let client = http_gun.named(name)
pub fn with_additional_cancellation(
  client: Client,
  token: cancellation.Token,
) -> Client

Add an independent cancellation token while retaining this view’s tokens. Cancellation by any retained token ends unfinished requests through this view. Adding the same token twice has no additional effect. This pure view operation starts no process, changes no other view and never cancels a token.

pub fn with_body_limit(
  client: Client,
  bytes: Int,
  overflow: Overflow,
) -> Client

Collect at most bytes of each response body in send and batch through this view, replacing config.with_max_response_body_bytes, and choose what happens past the limit. A negative limit fails each request with InvalidRequest(InvalidBodyLimit) before it is sent.

pub fn with_cancellation(
  client: Client,
  token: cancellation.Token,
) -> Client

Replace this view’s cancellation tokens with token. Unfinished requests through the resulting view cancel when that token is cancelled. Other views and tokens remain unchanged. Cancellation never enters cassette matching.

pub fn with_correlation(
  client: Client,
  correlation: correlation.Correlation,
) -> Client

Tag the lifecycle events of requests made through this view with the caller’s correlation, under the correlation metadata key, so they join the events of other packages working on the same unit. Every request still gets its own request_id. A later call replaces the correlation.

pub fn with_deadline(
  client: Client,
  deadline: deadline.Deadline,
) -> Client

End every request made through this view by an absolute deadline shared across calls. It replaces the client’s request timeout; combined with with_timeout, the earlier of the two applies.

pub fn with_destination(
  client: Client,
  policy: destination.Policy,
) -> Client

Narrow the destinations of this view: a request must satisfy the client’s policy and every policy added this way, including the addresses of a pooled connection it would reuse. A view can never widen what the client admits; host name resolution stays the client’s.

A client configured with config.require_view_destination refuses requests through a view unless one of its policies chooses a destination: it sets destination.only_hosts, or it refuses an address class the client admits. A policy that admits everything the client admits and only tightens destination.with_plaintext chooses none, so a library that tightens the scheme on a caller’s view never lifts the application’s requirement:

// A library: tightens the scheme, chooses no destination.
let scheme =
  destination.default()
  |> destination.allow_loopback
  |> destination.allow_private
  |> destination.with_plaintext(destination.PlaintextToLoopbackOnly)
client |> http_gun.with_destination(scheme)
pub fn with_idle_timeout(
  client: Client,
  timeout: config.Timeout,
) -> Client

Replace the client’s idle timeout for this view: the longest gap without bytes while the response’s owner waits for its head or reads its body. Use it for a model that thinks before its first token, or a stream with sparse keepalives.

pub fn with_response(
  client: Client,
  req: request.Request(BitArray),
  on_failure: fn(error.Failure) -> e,
  run: fn(response.Response(body.Body)) -> Result(a, e),
) -> Result(a, e)

Open a response, run run with it and close the body when run returns or raises. on_failure maps an opening failure into the callback’s error type:

use response <- http_gun.with_response(client, req, AppHttpError)
read_events(response.body)
pub fn with_timeout(
  client: Client,
  timeout: config.Timeout,
) -> Client

Bound each request made through this view from admission to its last byte, replacing the client’s request timeout, shorter or longer. config.Infinity lifts the bound for a long-lived stream; the connect, pool and idle timeouts still apply.

✨ Search Document