http: HTTP types and an I/O-free HTTP/1.1 parser for OCaml #
Warning. This is an experimental fork of cohttp. Do not use it in production; use cohttp instead.
An HTTP client and an HTTP server need the same vocabulary: headers, status
codes, methods, media types and bodies, as RFC
9110 defines them. They also
need to agree on where a message ends, and a server that frames a request
differently from the proxy in front of it can be tricked into reading two
requests as one. nox-http provides the shared types, and an HTTP/1.1
request parser (RFC 9112)
that refuses those request-smuggling shapes before the caller sees a request.
The core library does no I/O: it reads and writes through
bytesrw readers and writers. The
nox-http-eio library adds the Eio
side: errors raised as Eio.Io, and bodies built from files and flows.
Installation #
Install with opam:
$ opam install nox-http nox-http-eio
If opam cannot find the package, it may not yet be released in the public
opam-repository. Add the overlay repository, then install it:
$ opam repo add samoht https://tangled.org/gazagnaire.org/opam-overlay.git
$ opam update
$ opam install nox-http nox-http-eio
Modules #
The parsers and codecs for the wire:
Requestparses a request head and resolves its framing. The headers it returns are a zero-copy view over the input, and no header map is built unless the caller iterates over them.Responseparses a response head, and answers the caching questions of RFC 9111 about a received response.Chunkdecodes and encodes the chunked transfer coding (RFC 9112 Section 7.1). The encoder returns anox-roperope, so the payload is never copied, and forbidden trailer fields are dropped in both directions.Pathdecodes a request-target path (RFC 9112 Section 3.2). Each segment is percent-decoded exactly once. A segment that could climb out of the root is refused, where some servers would silently collapse it.Multipartparses amultipart/form-databody (RFC 7578).
The vocabulary of HTTP semantics:
Headersis a header collection with case-insensitive names, either a map or a zero-copy view over a parse buffer.Header_nametypes the registered header names.Status,MethodandVersioncover status codes, methods and protocol versions, with their ALPN identifiers. ALPN (Application-Layer Protocol Negotiation, RFC 7301) is the TLS extension in which the client lists the protocols it speaks, such ash2andhttp/1.1, and the server picks one during the handshake.Mimehandles media types andDatethe three HTTP date formats.Etagcompares entity-tags (RFC 9110 Section 8.8.3),Rangeresolves byte ranges to a 200, 206 or 416 answer (RFC 9110 Section 14), andCache_controlparses caching directives (RFC 9111).Bodybuilds request bodies from strings, readers, JSON, forms and multipart parts.Hurivalidates HTTP(S) URIs and serialises them to a bytesrw writer.Erroris the client error type, with the predicates a retry policy needs.Expect_continueandResponse_limitsconfigure a client's100-continuepolicy and the bounds on what a server can make it allocate.
Quick start #
A request body and its headers:
open Http
(* Build a JSON body. *)
let body =
Json.Value.(
Body.json (object' [ member (name "name") (string "Alice") ]))
(* Build headers. *)
let headers =
Headers.empty
|> Headers.set `Content_type "application/json"
|> Headers.set `Authorization "Bearer token"
|> Headers.set (`Other "X-Request-Id") "abc123"
(* Multipart form. A part's content is a string or a pull reader; the Eio
conveniences (a part from a file or a flow) live in nox-http-eio. *)
let upload () =
Body.multipart
[
{
Body.name = "field";
filename = None;
content_type = Mime.text;
content = `String "value";
};
]
Parsing requests #
Request.of_string (or Request.of_reader) parses a request head and
resolves its framing. Before it returns Complete it refuses the known
request-smuggling shapes: Content-Length sent together with
Transfer-Encoding, two lengths that disagree, or a bare CR or LF. A header
lookup scans the input buffer and allocates nothing.
open Http
let () =
let raw =
"POST /items?page=2 HTTP/1.1\r\n\
Host: example.com\r\n\
Content-Type: application/json\r\n\
Content-Length: 0\r\n\
\r\n"
in
match Request.of_string raw with
| Request.Complete req ->
assert (req.meth = `POST);
assert (req.target = "/items?page=2");
assert (req.host = Some "example.com");
(* zero-copy lookup -- no header map is built *)
assert (Headers.string "content-type" req.headers = Some "application/json");
assert (req.body = Request.Fixed 0)
| Request.Partial -> () (* read more bytes and retry *)
| Request.Failed _ -> () (* malformed or smuggling -> respond 400/413/505 *)
License #
ISC