RequestSeal.Message (RequestSeal v0.2.0)

Copy Markdown View Source

Lossless HTTP message values for later component derivation.

new/1 accepts a map. Required for both kinds: :kind (:request or :response), ordered :fields of RequestSeal.FieldOccurrence, :trailers, a RequestSeal.Body, and :transport (RequestSeal.TransportFacts). Trailers are an ordered list (including an observed empty list), :unavailable, or :pending while the body is streaming. A streaming body describes unread caller-owned content, not transport progress: a completed capture may have known trailers while its content stream remains unread. Header and trailer sections never mix. Total fields and trailers are bounded to 1,024 occurrences and 1,048,576 bytes of names and values. Field maps, tuples, and invalid nested structs reject.

Requests require a token :method (at most 256 bytes), :raw_target (at most 16,384 bytes), and explicit :target_form: :origin, :absolute, :authority (CONNECT with a port), or :asterisk (OPTIONS). URI octets and percent-encoding remain unchanged; fragments, userinfo, malformed escapes, and controls reject. Absolute targets require a scheme and authority. Optional :scheme and :authority are a pair of caller-declared authoritative origin facts, never inferred from Host or Forwarded. Scheme is bounded to 64 bytes; authority to 1,024 bytes. Ports are decimal numbers in 0..65535. Missing origin is preserved as nil; a later component rule decides whether it is required. When supplied, an absolute target must match the declared scheme and authority byte for byte; a CONNECT target must match the declared authority. No normalization is used to reconcile conflicting declarations.

Responses require integer :status in 100..599. Optional :related_request is a validated request (not another response); absent context remains nil. Request-only fields must be nil on responses. Requests cannot carry status or another related request. Unknown options reject; no normalization occurs.

validate/1 applies the same checks to directly constructed or modified structs. Success establishes a well-formed value, not HTTP framing, component availability, cryptographic validity, or authentication. Signature-base derivation is provided by RequestSeal.SignatureBase. Generic signing and verification are provided by RequestSeal; optional Req/Finch adapters, Plug integration (also used by Phoenix), Ash scope mapping, and Web Bot Auth protocol-00 are implemented. Other named application profiles use RequestSeal.Profile in extension packages. This module starts no process and reads no stream. Default inspection is redacted.

iex> {:ok, body} = RequestSeal.Body.new(%{state: :unavailable})
iex> {:ok, transport} = RequestSeal.TransportFacts.new(%{})
iex> {:ok, request} = RequestSeal.Message.new(%{kind: :request, method: "GET", raw_target: "/a%2Fb?", target_form: :origin, fields: [], trailers: :unavailable, body: body, transport: transport})
iex> {request.raw_target, request.body.state, request.scheme}
{"/a%2Fb?", :unavailable, nil}
iex> RequestSeal.Message.validate(request)
:ok
iex> RequestSeal.Message.new(%{kind: :request})
{:error, %RequestSeal.Message.Error{reason: :invalid_message}}

Summary

Functions

Construct a request or response; return a bounded error on invalid input.

Build a request from an absolute HTTP(S) URL, ordered headers, and exact body bytes.

Build a response with ordered headers and retained bytes (nil means empty).

Validate all nested values, states, bounds, targets, and request linkage.

Types

t()

@type t() :: %RequestSeal.Message{
  authority: binary() | nil,
  body: RequestSeal.Body.t(),
  fields: [RequestSeal.FieldOccurrence.t()],
  kind: :request | :response,
  method: binary() | nil,
  raw_target: binary() | nil,
  related_request: t() | nil,
  scheme: binary() | nil,
  status: 100..599 | nil,
  target_form: :origin | :absolute | :authority | :asterisk | nil,
  trailers: [RequestSeal.FieldOccurrence.t()] | :unavailable | :pending,
  transport: RequestSeal.TransportFacts.t()
}

Functions

new(attrs)

@spec new(map()) :: {:ok, t()} | {:error, RequestSeal.Message.Error.t()}

Construct a request or response; return a bounded error on invalid input.

request(method, url, headers, bytes, opts \\ [])

@spec request(binary(), binary(), [{binary(), binary()}], binary() | nil, keyword()) ::
  {:ok, t()} | {:error, RequestSeal.Message.Error.t()}

Build a request from an absolute HTTP(S) URL, ordered headers, and exact body bytes.

Scheme and host are lowercased; default ports (HTTPS 443 and HTTP 80) are omitted, matching Finch and Req. Percent escapes and query bytes are preserved. An absent path becomes /; an empty query retains its ?. Header case, order, and repeats remain unchanged. nil and "" both retain empty content. Transport declarations stay unknown and trailers unavailable. All values pass through new/1, including its 1 MiB body retention limit.

The only option is :digest, default nil: a nonempty unique list of "sha-256" and/or "sha-512" adds Content-Digest over the exact bytes. Existing Content-Digest headers reject when generating a digest; omit the option to preserve caller-supplied headers. Invalid options return :invalid_message; invalid headers and bodies retain their existing errors.

iex> {:ok, message} = RequestSeal.Message.request("post", "https://example.com:8443/a%2Fb?", [{"X", "one"}, {"X", "two"}], nil)
iex> {message.method, message.authority, message.raw_target, message.body.bytes}
{"post", "example.com:8443", "/a%2Fb?", ""}
iex> Enum.map(message.fields, &{&1.name, &1.value})
[{"X", "one"}, {"X", "two"}]

response(status, headers, bytes, opts \\ [])

@spec response(100..599, [{binary(), binary()}], binary() | nil, keyword()) ::
  {:ok, t()} | {:error, RequestSeal.Message.Error.t()}

Build a response with ordered headers and retained bytes (nil means empty).

Accepts the same :digest option as request/5. Optional :request links a validated request for components with req; no linkage is inferred. Status, fields, body, and related request pass through new/1 unchanged.

iex> {:ok, request} = RequestSeal.Message.request("GET", "https://example.com/", [], nil)
iex> {:ok, response} = RequestSeal.Message.response(204, [], nil, request: request)
iex> {response.status, response.related_request == request, response.body.bytes}
{204, true, ""}

validate(message)

@spec validate(term()) :: :ok | {:error, RequestSeal.Message.Error.t()}

Validate all nested values, states, bounds, targets, and request linkage.