Expand description
A minimal blocking HTTP/1.1 client over any Read + Write.
This is the single highest-leverage minimalism decision: one
~250-line module replaces the ureq/url→IDNA→ICU dependency tax. It
carries the intelligence wire over TCP, TLS, unix sockets, and vsock
alike — the transport is just the stream.
Two request paths: send buffers the whole response (the LLM/intelligence
path), and send_streaming returns the status + headers plus a live reader
so the caller can either buffer it (application/json) or pump it as an SSE
stream (SseReader) — the MCP Streamable HTTP transport, where a response may
be a single JSON body or a text/event-stream, and a long-lived GET carries
server→client notifications.
connect_tcp is intentionally unguarded, and dials by name: it exists for
the operator-configured endpoints (the intelligence dial, the auth/token
endpoints), which the model cannot influence. A model/agent/peer-supplied URL
must NOT be dialled through it — not even with a guard wrapped around it,
since the guard’s lookup and this function’s lookup are two resolutions and a
hostile nameserver can answer them differently. Those surfaces use
crate::ssrf::connect_vetted, which resolves once and dials the address it
vetted.
Structs§
- Response
- A parsed HTTP response.
- SseEvent
- One parsed
text/event-streamevent. For MCP,datais a JSON-RPC message;event/idare the optional SSE field lines. - SseReader
- A blocking, line-based SSE reader.
next_eventaccumulatesfield: valuelines and emits oneSseEventper blank-line separator (multipledata:lines join with\n), returningOk(None)at end of stream. Bounded per event byMAX_RESPONSEso a hostile stream cannot exhaust memory. - Streaming
Response - A streamed response: status + headers, plus the reader positioned at the body.
The caller decides how to drain it — [
into_body] to buffer (application/json) or [sse] to pump it as an SSE event stream (text/event-stream). Owns the underlying stream, matching the MCP client’s per-request connection model. - Url
- A parsed absolute URL (the subset we need: scheme/host/port/path).
Constants§
- MAX_
RESPONSE - Response body cap. LLM responses can be large; 8 MiB is generous without being an unbounded allocation from a hostile peer.
Traits§
- Stream
- Any bidirectional byte stream the HTTP client can run over.
Box<dyn Stream>is itselfRead + Write(via std’simpl<R: Read + ?Sized> Read for Box<R>), so an OWNED boxed stream can be handed tosend_streamingby value — used by the long-lived MCP notification SSE reader.
Functions§
- connect_
tcp - Connect a plain TCP stream with connect + read/write timeouts. Intentionally
unguarded, and resolves the name itself — for operator-configured endpoints
only. A model/agent/peer-supplied URL belongs on
crate::ssrf::connect_vetted: composingssrf::guard_hostaround this function resolves twice and is the DNS-rebinding hole, not a fix for it. - is_
loopback_ host - Whether
hostnames the local loopback — the dev/test carve-out for plaintexthttp://(production transports are TLS-only). Accepts the IPv4 loopback block (127.0.0.0/8), the IPv6 loopback (::1, bare or bracketed), and the literal namelocalhost. A resolvable-but-unresolved name is NOT loopback — this classifies the written form, without DNS. - send
- Issue one request over
streamand read the full response. AddsHost,Connection: close, andContent-Length; the caller supplies any other headers (e.g.Authorization,Content-Type). - send_
streaming - Issue one request over an OWNED
streamand return the status + headers + body reader WITHOUT draining the body (unlikesend). AddsHost,Connection: close, andContent-Length; the caller supplies the rest (Accept,Authorization,Content-Type,Mcp-Session-Id, …).