My own corner of monopam
README.md

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:

  • Request parses 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.
  • Response parses a response head, and answers the caching questions of RFC 9111 about a received response.
  • Chunk decodes and encodes the chunked transfer coding (RFC 9112 Section 7.1). The encoder returns a nox-rope rope, so the payload is never copied, and forbidden trailer fields are dropped in both directions.
  • Path decodes 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.
  • Multipart parses a multipart/form-data body (RFC 7578).

The vocabulary of HTTP semantics:

  • Headers is a header collection with case-insensitive names, either a map or a zero-copy view over a parse buffer.
  • Header_name types the registered header names.
  • Status, Method and Version cover 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 as h2 and http/1.1, and the server picks one during the handshake.
  • Mime handles media types and Date the three HTTP date formats.
  • Etag compares entity-tags (RFC 9110 Section 8.8.3), Range resolves byte ranges to a 200, 206 or 416 answer (RFC 9110 Section 14), and Cache_control parses caching directives (RFC 9111).
  • Body builds request bodies from strings, readers, JSON, forms and multipart parts.
  • Huri validates HTTP(S) URIs and serialises them to a bytesrw writer.
  • Error is the client error type, with the predicates a retry policy needs.
  • Expect_continue and Response_limits configure a client's 100-continue policy 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