Nous.Providers.HTTP (nous v0.15.3)
View SourceShared HTTP utilities for all LLM providers.
Two HTTP families, deliberately split by use case:
- Non-streaming requests (one-shot model calls, web fetching, search
APIs) go through a pluggable
Nous.HTTP.Backend. The default isNous.HTTP.Backend.Req(Req on top of Finch).Nous.HTTP.Backend.Hackneyis also shipped — pick it via per-call:backendopt, theNOUS_HTTP_BACKENDenv var, orconfig :nous, :http_backend, .... Seedocs/benchmarks/http_backend.mdfor the trade-offs. - Streaming requests (SSE / chunked LLM responses) go through
:hackneyin:async, :oncepull-based mode. Hackney's:async, :onceis true pull-based streaming - the consumer calls:hackney.stream_next/1to ask for one more chunk, the producer reads it off the socket and delivers it as a single message. The consumer paces the producer, so a slow consumer (LiveView assigns + diff + push, slow IO, etc.) can never grow its mailbox unboundedly.
This split fixes M-12 (streaming consumer backpressure) and H-12 (stream
lifecycle EXIT handling) from the comprehensive review by eliminating
the spawn-and-mailbox plumbing entirely - the Stream.resource
consumer is the only process involved.
Usage
# Non-streaming request (Req + Finch)
{:ok, body} = HTTP.post(url, body, headers)
# Streaming request (hackney pull-based, returns lazy stream)
{:ok, stream} = HTTP.stream(url, body, headers)
Enum.each(stream, &process_event/1)SSE Parsing
SSE events follow the Server-Sent Events spec (https://html.spec.whatwg.org/multipage/server-sent-events.html):
- Events are separated by double newlines (
\n\n) - Each event contains field lines like
data: {...} - Multiple
data:fields are concatenated with newlines [DONE]signals stream completion (OpenAI convention)
Hackney pool
Hackney's :default pool starts automatically when the :hackney
application boots. Defaults: 50 max connections per pool, 2s idle
keepalive timeout. For most LLM workloads (long-lived streams of
seconds-to-minutes) the defaults are appropriate. Apps that need a
Nous-isolated pool can pass pool: :my_pool per request and start
the pool with :hackney_pool.start_pool/2 (or include
:hackney_pool.child_spec/2 in their supervision tree).
TLS verification
Streaming requests pass verify: :verify_peer with system CAs from
:public_key.cacerts_get/0 explicitly. Do not silently regress this -
hackney would otherwise default to :verify_none and accept MITM'd
connections.
Summary
Functions
Build authorization header for API key auth (Anthropic style).
Build authorization header for Bearer token auth (OpenAI style).
Parse an SSE buffer into events.
Parse a single SSE event.
Make a non-streaming POST request.
Make a streaming POST request with SSE parsing.
Functions
Build authorization header for API key auth (Anthropic style).
Returns empty list for nil or empty string values.
Build authorization header for Bearer token auth (OpenAI style).
Returns empty list for nil, empty string, or "not-needed" values.
@spec parse_sse_buffer(String.t() | nil | any()) :: {list(), String.t()} | {:error, :buffer_overflow}
Parse an SSE buffer into events.
Returns {events, remaining_buffer} where events is a list of parsed
JSON maps, {:stream_done, reason} tuples, or {:parse_error, reason} tuples.
Handles edge cases:
- Empty events (ignored)
- Whitespace-only events (ignored)
- Malformed JSON (emits
{:parse_error, reason}) - Multiple data fields per event (concatenated per spec)
- Comment lines (ignored)
- Buffer overflow protection
Examples
iex> parse_sse_buffer("data: {\"text\": \"hi\"}\n\n")
{[%{"text" => "hi"}], ""}
iex> parse_sse_buffer("data: partial")
{[], "data: partial"}
iex> parse_sse_buffer("data: [DONE]\n\n")
{[{:stream_done, "stop"}], ""}
@spec parse_sse_event(String.t()) :: map() | {:stream_done, String.t()} | {:parse_error, term()} | nil
Parse a single SSE event.
Returns parsed JSON map, {:stream_done, reason}, {:parse_error, reason}, or nil.
Handles per SSE spec:
data:fields (with or without space after colon)- Multiple
data:fields concatenated with newlines :prefix for comments (ignored)event:,id:,retry:fields (ignored for now)- Empty lines within events
Examples
iex> parse_sse_event("data: {\"key\": \"value\"}")
%{"key" => "value"}
iex> parse_sse_event("data: [DONE]")
{:stream_done, "stop"}
iex> parse_sse_event(": this is a comment")
nil
iex> parse_sse_event("")
nil
Make a non-streaming POST request.
Dispatches to the configured Nous.HTTP.Backend. Resolution order
(highest precedence first):
- Per-call
:backendopt —HTTP.post(url, body, headers, backend: Nous.HTTP.Backend.Hackney) NOUS_HTTP_BACKENDenv var —req,hackney, or a fully-qualified module name (e.g.MyApp.MyHTTPBackend)Application.get_env(:nous, :http_backend, ...)- Default:
Nous.HTTP.Backend.Req
Returns {:ok, body} or {:error, reason}.
Options
:backend- Backend module (overrides env / config / default):timeout- Request timeout in ms (default: 60_000)
Error Reasons
%{status: integer(), body: term()}- HTTP error response%Mint.TransportError{}- Network error (Req backend)%JSON.DecodeError{}- JSON decode error
Make a streaming POST request with SSE parsing.
Returns {:ok, stream} where stream is an Enumerable of parsed events.
Events are maps with string keys (parsed JSON) or {:stream_done, reason} tuples.
Options
:timeout- Receive timeout in ms (default: 60_000) — passed to hackney as:recv_timeout.:connect_timeout- TCP connect timeout in ms (default: 30_000).:pool- Hackney pool name (default::default). Configure the default pool viaconfig :nous, :hackney_pool, max_connections: ..., timeout: ...or start a dedicated pool with:hackney_pool.start_pool/2.:stream_parser- Module for parsing the stream buffer (default: SSE parsing). Must implementparse_buffer/1returning{events, remaining_buffer}. SeeNous.Providers.HTTP.JSONArrayParserfor an example.:finch_name- Ignored. Kept for source compatibility with callers from before the 0.15.0 hackney rewrite. Will be removed in a future release.
Error Handling
The stream will emit {:stream_error, reason} on errors and then halt.