hyper-uds speaks a strict subset of HTTP/1.1 over an AF_UNIX
SOCK_STREAM socket. The on-the-wire bytes are ordinary HTTP, so
curl --unix-socket and socat work for debugging. The only
hyper-uds-specific element is the X-FD header, which is paired with
SCM_RIGHTS ancillary data on the underlying sendmsg / recvmsg
syscalls.
This document is the normative description of what the server accepts and emits.
- Socket family:
AF_UNIX. - Socket type:
SOCK_STREAM. - Byte stream: HTTP/1.1.
- Ancillary:
SCM_RIGHTS(seefd-passing.md).
TCP/IP and SOCK_DGRAM are not supported and never will be — the whole
point is SCM_RIGHTS, which only AF_UNIX SOCK_STREAM (and
SOCK_SEQPACKET) carry.
METHOD SP request-target SP HTTP-version CRLF
METHOD— any token accepted byhttp::Method::from_bytes. Routing is the userService's job; the framework does not special-case any method.request-target— any path/query accepted byhttp::Uri::from_str. Authority-form (used by HTTPCONNECT) is parsed but has no special meaning here.HTTP-version—HTTP/1.0orHTTP/1.1. Anything else is rejected withError::Parse("unsupported http version"). See §5 for the asymmetry: 1.0 input is accepted, but the server always responds withHTTP/1.1.
- Up to 64 headers per request (
MAX_HEADERSincodec.rs). Exceeding this returnsError::HeadTooLarge. - Total head size — the request line plus the entire header block plus
the terminating
CRLF CRLF— must not exceedBuilder::max_head_size(default 16 KiB). Exceeding this also returnsError::HeadTooLarge. - Header names and values must be valid per RFC 9110; the framework
rejects malformed headers with
Error::Parse.
The framework reads three headers specially. All other headers are
preserved in Request::headers() verbatim and are the user's to
interpret.
- If present, must be a valid base-10 unsigned integer.
- Defines the exact body length the server will read.
- If
Content-Length > Builder::max_body_size(default 16 MiB) the server returnsError::BodyTooLargeand closes the connection before reading the body. - If absent, the request body is treated as empty (length 0). This diverges from RFC 9112 (which would imply chunked or close-delimited); see §3 below.
- Comma-separated tokens; matched case-insensitively.
closetoken — keep-alive is disabled for this connection. After responding, the server closes the socket.keep-alivetoken — only meaningful with HTTP/1.0 input; explicitly opts in to keep-alive. With HTTP/1.1 input, keep-alive is the default unlesscloseis present.Builder::keep_alive(false)overrides everything: every connection is one-shot regardless of what the request says.
- Comma-separated list of logical descriptor names.
- Each name is
trim()ed of ASCII whitespace; empty names (e.g. an extra comma) yieldError::InvalidXFdHeader. - The order of names defines the order of expected descriptors in the
ancillary
SCM_RIGHTScmsg.fd_names[i]↔fds[i]. - The header must be valid UTF-8.
If X-FD names N descriptors but fewer than N descriptors arrived
on the cmsg, the request is rejected with Error::InvalidXFdHeader.
If more than N descriptors arrived, the surplus is silently closed.
This lets clients keep a stable header layout while attaching extra
context (e.g. a debug pidfd) that the server isn't expected to know
about. To receive all attached fds, the client must list every one in
X-FD.
If more than Builder::max_fds_per_request (default 16) descriptors
arrived in total — including the surplus — the connection is aborted
with Error::TooManyFds. This bound is enforced before the
surplus trim; clients cannot bypass the cap by leaving fds out of the
header.
- Length is determined exclusively by
Content-Length. - No chunked encoding. A request that arrives with
Transfer-Encoding: chunkedwill not be parsed correctly; the bytes after the head will be interpreted as the start of the next request and almost certainly produce anError::ParseorError::HeadTooLarge. This is by design — see "Why no chunked" below. - The body is buffered in memory in full before the
Serviceis called. There is no streaming-body API.
For local IPC, payload sizes are virtually always known up front by
the client (RPC arguments are not unbounded streams). Chunked
encoding doubles the framing-state-machine surface and complicates the
"one head + one body, then maybe loop" flow that makes the connection
state machine trivially auditable. curl -d ... defaults to
Content-Length for buffered bodies, so debug compatibility is
preserved for the common case.
If chunked-style streaming becomes necessary later, the right answer
is a separate response body API on Response, not retrofitting it
through the existing single-Bytes shape.
HTTP/1.1 SP status-code SP reason-phrase CRLF
- The framework always writes
HTTP/1.1regardless of request version. (Downgrading toHTTP/1.0is not implemented; a real 1.0 client surfacing this would be the trigger to add it.) status-codeisResponse::status().as_str().reason-phraseis the canonical IANA reason for the status, or empty string if unknown.
The framework owns three response headers and strips any user-set copies of them before writing user headers. This prevents duplicates:
Content-Length— always emitted, computed fromResponse.body.len(). Bodies are fully buffered, so the length is always known.Connection— emitted asConnection: closeif and only if the computed keep-alive state for this exchange isfalse. Otherwise omitted (HTTP/1.1's default is keep-alive).X-FD— emitted if and only ifResponse.fd_namesis non-empty. The names are joined with,and emitted in the same order as the attached descriptors (seefd-passing.md).
All other headers from Response::headers() are written through
unchanged.
- Always
Content-Length-framed. - Written to the socket immediately after the head's terminating
CRLF CRLF.
The effective keep-alive state for a request/response cycle is the boolean AND of:
Builder::keep_alive(...)— server policy. Defaulttrue.- The wire policy:
- HTTP/1.1: keep-alive unless the request had
Connection: close. - HTTP/1.0: connection-close unless the request had
Connection: keep-alive.
- HTTP/1.1: keep-alive unless the request had
If keep-alive is false, the server emits Connection: close in the
response and closes the socket after writing.
Connection::graceful_shutdown() causes the next iteration of the
read loop to return Ok(()) instead of reading another request. The
in-flight request, if any, completes normally. There is no abrupt
mid-request cancellation API.
| Request version | Accepted? | Response version | Default keep-alive |
|---|---|---|---|
HTTP/1.0 |
yes | HTTP/1.1 |
off |
HTTP/1.1 |
yes | HTTP/1.1 |
on |
HTTP/2.0 etc. |
no — Error::Parse |
n/a | n/a |
The asymmetric "accept 1.0, respond 1.1" stance is a debug-affordance choice: many old curl-style toolchains default to 1.0, and rejecting them entirely is unfriendly; but emitting 1.0 responses requires implementing the 1.0 keep-alive grammar more carefully than is worth it for this crate's use-case.
The framework currently does not translate parse / size / fd errors
into HTTP error responses. Any failure in the read or fd-handoff
phases returns an Error from the Connection future, and the
underlying socket is dropped. The rationale is:
- An IPC peer who sends a malformed head is very likely a bug, not a
recoverable HTTP client. Returning
400 Bad Requestwould let the same bug recur on the next iteration. - Returning a body that mentions a structured framework error leaks internal taxonomy across a process boundary that callers should not depend on.
If a service wants to return 4xx/5xx for application-level
problems, it does so by returning an Ok(Response { status: ... })
from Service::call, exactly like hyper. Returning Err(...) from
Service::call wraps the error in Error::Service and aborts the
connection.
A request with one attached fd named input and an empty body:
POST /open HTTP/1.1
host: localhost
content-length: 0
x-fd: input
Sent as one sendmsg whose cmsghdr carries
SCM_RIGHTS [<input_fd>].
A response that returns one fd named reply with a 2-byte body:
HTTP/1.1 200 OK
content-length: 2
x-fd: reply
ok
Sent as one or more sendmsg calls; the first carries
SCM_RIGHTS [<reply_fd>], subsequent retries (after partial writes)
carry no cmsg.
This exact handshake is what tests/e2e.rs::fd_passing_round_trip
exercises end-to-end against a hand-rolled recvmsg/sendmsg client.