h11

    A pure, bring-your-own-I/O implementation of HTTP/1.1 (port of python-hyper/h11)

    http
    http11
    sans-io
    protocol
    Download zip
    Author
    Version
    0.1.1
    License
    MIT
    Last updated
    yesterday
    Downloads
    6

    Dependencies

    #bobzhang/h11

    A pure, bring-your-own-I/O implementation of HTTP/1.1 for MoonBit — a faithful port of Python's h11.

    • Sans-I/O. h11 never touches a socket. You feed it bytes, it gives you events; you give it events, it gives you bytes. Use it with any runtime: moonbitlang/async, a custom event loop, a test harness, or a fuzzer.
    • Complete HTTP/1.1 semantics. Request/response framing (Content-Length, chunked encoding, HTTP/1.0 read-until-close), keep-alive and pipelining, Expect: 100-continue, Upgrade and CONNECT protocol switching, HEAD responses, trailers, and strict validation of everything that goes on or comes off the wire.
    • A real state machine. Both peers' states are tracked explicitly, so protocol violations are caught as errors instead of silently producing garbage. Errors carry a suggested HTTP status code.
    • Thoroughly tested. The whole h11 test suite is ported and passes.
    • No dependencies beyond the MoonBit standard library; works on every backend.

    #Installation

    moon add bobzhang/h11

    Then import it in your package's moon.pkg:

    import { "bobzhang/h11", }

    #Quick start

    A client and a server talking to each other entirely in memory:

    ///|
    test "quick start: one request/response cycle" {
    let client = @h11.Connection::new(Client)
    let server = @h11.Connection::new(Server)

    // The client turns events into bytes...
    let request = @h11.Request::new(method_=b"GET", target=b"/hello", headers=[
    (b"Host", b"example.com"),
    ])
    let wire = client.send(Request(request)).unwrap() +
    client.send(EndOfMessage(@h11.EndOfMessage::new())).unwrap()
    inspect(
    @utf8.decode(wire),
    content="GET /hello HTTP/1.1\r\nHost: example.com\r\n\r\n",
    )

    // ...and the server turns bytes back into events.
    server.receive_data(wire)
    guard server.next_event() is Event(Request(req)) else { fail("no request") }
    assert_eq(req.target, b"/hello")
    guard server.next_event() is Event(EndOfMessage(_)) else { fail("no EOM") }
    assert_eq(server.next_event(), NeedData)

    // The server replies. No Content-Length was given, so h11 picks chunked
    // transfer encoding automatically because the client speaks HTTP/1.1.
    let response = @h11.Response::new(status_code=200, headers=[
    (b"Content-Type", b"text/plain"),
    ])
    let wire = server.send(Response(response)).unwrap() +
    server.send(Data(@h11.Data::new(b"hi!"))).unwrap() +
    server.send(EndOfMessage(@h11.EndOfMessage::new())).unwrap()
    inspect(
    @utf8.decode(wire),
    content="HTTP/1.1 200 \r\nContent-Type: text/plain\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nhi!\r\n0\r\n\r\n",
    )

    // The client parses the response.
    client.receive_data(wire)
    guard client.next_event() is Event(Response(resp)) else { fail("no resp") }
    assert_eq(resp.status_code, 200)
    guard client.next_event() is Event(Data(body)) else { fail("no data") }
    assert_eq(body.data, b"hi!")
    guard client.next_event() is Event(EndOfMessage(_)) else { fail("no EOM") }

    // Both sides are DONE, so the connection can be reused.
    assert_eq(client.states(), { client: Done, server: Done, })
    client.start_next_cycle()
    server.start_next_cycle()
    }

    #How it works

    A Connection is created with the role you are playing (Client or Server) and has three core operations:

    OperationWhat it does
    conn.receive_data(bytes)Append bytes you read from the network to the internal buffer. Pass b"" to signal end-of-file.
    conn.next_event()Parse the next event from the buffer. Returns Event(event), NeedData (read more from the socket), or Paused (the peer is done for now; see Keep-alive).
    conn.send(event)Validate event against the state machine and return the bytes to write (None for ConnectionClosed).

    The events are:

    EventMeaning
    Request(Request)Start of a request: method_, target, headers, http_version
    InformationalResponse(InformationalResponse)A 1xx response
    Response(Response)Start of a final response: status_code, headers, http_version, reason
    Data(Data)A piece of a message body
    EndOfMessage(EndOfMessage)End of a message body, with optional trailers
    ConnectionClosedThe peer closed their side of the connection

    Event payloads are built with validating constructors (Request::new, Response::new, ...) that raise LocalProtocolError if you try to build something illegal.

    #A server loop

    Here is the shape of a typical server, with the network abstracted as a list of chunks that arrive one by one:

    ///|
    /// Handle one connection whose incoming bytes arrive as `chunks`, returning
    /// everything the server wrote.
    fn serve(chunks : Array[Bytes]) -> Bytes raise {
    let conn = @h11.Connection::new(Server)
    let out = @buffer.Buffer()
    let mut next_chunk = 0
    for ;; {
    match conn.next_event() {
    NeedData =>
    // read from the socket; b"" means EOF
    if next_chunk < chunks.length() {
    conn.receive_data(chunks[next_chunk])
    next_chunk 1
    } else {
    conn.receive_data(b"")
    }
    Event(Request(req)) => {
    let body = b"you asked for " + req.target
    let length = @utf8.encode(body.length().to_string())
    let resp = @h11.Response::new(status_code=200, headers=[
    (b"Content-Length", length),
    ])
    out.write_bytes(conn.send(Response(resp)).unwrap())
    out.write_bytes(conn.send(Data(@h11.Data::new(body))).unwrap())
    out.write_bytes(
    conn.send(EndOfMessage(@h11.EndOfMessage::new())).unwrap(),
    )
    }
    Event(ConnectionClosed) => break
    Event(_) => () // request body chunks, end of request, ...
    Paused =>
    // Both sides finished one request/response cycle.
    if conn.our_state() is Done && conn.their_state() is Done {
    conn.start_next_cycle()
    } else {
    break // e.g. MUST_CLOSE: we should close the socket
    }
    }
    }
    out.to_bytes()
    }

    ///|
    test "server loop with a pipelined, fragmented request stream" {
    let replies = serve([
    b"GET /a HTTP/1.1\r\nHost: x\r\n\r\nGET /b HT", b"TP/1.1\r\nHost: x\r\n\r\n",
    ])
    inspect(
    @utf8.decode(replies),
    content="HTTP/1.1 200 \r\nContent-Length: 16\r\n\r\nyou asked for /aHTTP/1.1 200 \r\nContent-Length: 16\r\n\r\nyou asked for /b",
    )
    }

    #Keep-alive, pipelining, and Paused

    After a peer finishes its part of a request/response cycle, next_event returns Paused if more data is already buffered (a pipelined request). Once both sides reach Done, call start_next_cycle() to reset to Idle and continue reading. If either side sends Connection: close or speaks HTTP/1.0, the states become MustClose instead and you should close the socket after sending your response. h11 adds Connection: close to your responses automatically when it is required.

    #Body framing is handled for you

    • A message with Content-Length is checked to contain exactly that many bytes (sending too much or too little is a LocalProtocolError).
    • A response without Content-Length to an HTTP/1.1 client uses Transfer-Encoding: chunked; to an HTTP/1.0 client it falls back to "read until close" and forces Connection: close.
    • Responses to HEAD, 204, 304, and successful CONNECT never have a body, regardless of headers.

    ///|
    test "HTTP/1.0 peers get close-delimited bodies" {
    let server = @h11.Connection::new(Server)
    server.receive_data(b"GET / HTTP/1.0\r\n\r\n")
    guard server.next_event() is Event(Request(_)) else { fail("no request") }
    guard server.next_event() is Event(EndOfMessage(_)) else { fail("no EOM") }
    let wire = server.send(
    Response(@h11.Response::new(status_code=200, headers=[])),
    )
    inspect(
    @utf8.decode(wire.unwrap()),
    content="HTTP/1.1 200 \r\nConnection: close\r\n\r\n",
    )
    assert_eq(server.our_state(), SendBody)
    }

    #Headers

    Headers are an ordered list of (name, value) byte-string pairs. Iterating a Headers yields lowercased names; raw_items() preserves the original casing, which is also what goes on the wire.

    ///|
    test "headers keep their casing on the wire" {
    let headers = @h11.Headers::new([
    (b"Content-Type", b"text/html"),
    (b"X-Custom", b"1"),
    ])
    assert_eq(headers[0], (b"content-type", b"text/html"))
    assert_eq(headers.raw_items()[1], (b"X-Custom", b"1"))
    // Comma-separated headers can be read case-insensitively:
    let h = @h11.Headers::new([(b"Connection", b"Keep-Alive, Upgrade")])
    assert_eq(@h11.get_comma_header(h, b"connection"), [b"keep-alive", b"upgrade"])
    }

    #Expect: 100-continue

    ///|
    test "100-continue" {
    let server = @h11.Connection::new(Server)
    server.receive_data(
    b"POST /upload HTTP/1.1\r\nHost: x\r\nContent-Length: 4\r\nExpect: 100-continue\r\n\r\n",
    )
    guard server.next_event() is Event(Request(_)) else { fail("no request") }
    assert_true(server.they_are_waiting_for_100_continue())
    let wire = server.send(
    InformationalResponse(
    @h11.InformationalResponse::new(status_code=100, headers=[]),
    ),
    )
    inspect(@utf8.decode(wire.unwrap()), content="HTTP/1.1 100 \r\n\r\n")
    assert_true(!server.they_are_waiting_for_100_continue())
    }

    #Protocol switching (Upgrade / CONNECT)

    When the client proposes a switch, it enters MightSwitchProtocol after its request and next_event returns Paused until the server answers. If the server accepts (101 Switching Protocols for Upgrade, 2xx for CONNECT), both sides enter SwitchedProtocol and h11 steps aside: any bytes after the handshake are available from trailing_data() for the new protocol (e.g. WebSocket).

    ///|
    test "upgrading to another protocol" {
    let server = @h11.Connection::new(Server)
    server.receive_data(
    b"GET /chat HTTP/1.1\r\nHost: x\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n\x81\x05hello",
    )
    guard server.next_event() is Event(Request(_)) else { fail("no request") }
    guard server.next_event() is Event(EndOfMessage(_)) else { fail("no EOM") }
    assert_eq(server.next_event(), Paused)
    assert_eq(server.their_state(), MightSwitchProtocol)
    let accept = @h11.InformationalResponse::new(status_code=101, headers=[
    (b"Upgrade", b"websocket"),
    (b"Connection", b"Upgrade"),
    ])
    ignore(server.send(InformationalResponse(accept)))
    assert_eq(server.states(), {
    client: SwitchedProtocol,
    server: SwitchedProtocol,
    })
    // The bytes that followed the handshake belong to the new protocol.
    assert_eq(server.trailing_data(), (b"\x81\x05hello", false))
    }

    #Error handling

    • next_event raises RemoteProtocolError when the peer violates the protocol. The peer's state becomes Error; you should close the connection, optionally after sending an error response (the error's error_status_hint() suggests a status code).
    • send and the event constructors raise LocalProtocolError when you try to do something illegal. After a failed send, our state becomes Error.
    • Call send_failed() if writing to the socket fails, so the state machine knows the message was not delivered.

    ///|
    test "errors carry a suggested status code" {
    let server = @h11.Connection::new(Server)
    server.receive_data(
    b"GET / HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: gzip\r\n\r\n",
    )
    try server.next_event() catch {
    RemoteProtocolError(msg, error_status_hint~) => {
    inspect(msg, content="Only Transfer-Encoding: chunked is supported")
    assert_eq(error_status_hint, 501)
    }
    e => fail("unexpected error \{e}")
    } noraise {
    _ => fail("expected an error")
    }
    assert_eq(server.their_state(), Error)

    // We can still tell the client what went wrong:
    let wire = server.send(
    Response(@h11.Response::new(status_code=501, headers=[])),
    )
    inspect(
    @utf8.decode(wire.unwrap()),
    content="HTTP/1.1 501 \r\nConnection: close\r\n\r\n",
    )
    }

    h11 also limits how many bytes it will buffer while waiting for a complete request/response head (max_incomplete_event_size, default 16 KiB); exceeding it raises RemoteProtocolError with a 431 hint.

    #Coming from Python h11

    PythonMoonBit
    h11.Connection(our_role=h11.CLIENT)@h11.Connection::new(Client)
    conn.next_event() → event, h11.NEED_DATA, h11.PAUSEDconn.next_event() → Event(e), NeedData, Paused
    conn.send(event) → bytes or Noneconn.send(event) → Some(bytes) or None
    conn.states, conn.our_state, conn.their_stateconn.states(), conn.our_state(), conn.their_state()
    conn.their_http_version, conn.trailing_dataconn.their_http_version(), conn.trailing_data()
    h11.Request(method=..., target=..., headers=[...])@h11.Request::new(method_=..., target=..., headers=[...])
    h11.Data(data=b"..."), h11.EndOfMessage()@h11.Data::new(b"..."), @h11.EndOfMessage::new()
    h11.ConnectionClosed()ConnectionClosed
    h11.CLIENT, h11.SEND_BODY, h11.MUST_CLOSE, ...Client, SendBody, MustClose, ...
    except h11.RemoteProtocolError as e: e.error_status_hintcatch { RemoteProtocolError(msg, error_status_hint~) => ... }

    Differences worth knowing:

    • All wire values (methods, targets, header names and values, versions) are Bytes. The request method field is method_ because method is a reserved word in MoonBit.
    • Data.data is always Bytes. send_with_data_passthrough still guarantees that the exact Bytes you passed appears in the returned list, so you can swap in zero-copy writes.
    • Content-Length values and chunk sizes are tracked as Int64. The same inputs as Python are accepted (up to 20 digits); values beyond 2^63 - 1 saturate, which is unobservable in practice.
    • Data.chunk_start is true on the first data of every chunk. Python h11 reports False when a chunk header and its data arrive in separate reads.
    • receive_data after EOF raises RuntimeError (a MoonBit suberror).
    • Error messages quote received data as b'...' (Python shows bytearray(b'...')).

    #Development

    moon test

    The test suite has three layers:

    • a port of h11's own tests (*_test.mbt for the public API, *_wbtest.mbt for internals such as the state machine, readers, writers, and receive buffer);
    • QuickCheck properties (quickcheck_test.mbt): round trips, fragmentation invariance on valid, mutated, and garbage input, and robustness;
    • a differential fuzzer (fuzz/) that runs random and mutated byte streams through both this port and Python h11 and compares the resulting events, states, bytes sent, and error messages:

    python3 fuzz/difftest.py --cases 100000 --seed 1

    Python h11 is patched in the harness with the same chunk_start fix, so the comparison is exact; --unpatched compares against upstream as is.

    Every code block in this README also runs as a test.

    #License and credits

    MIT. Original h11 by Nathaniel J. Smith and contributors (python-hyper/h11); this is an independent MoonBit port.

    ProtocolError

    pub(all) suberror ProtocolError {
    LocalProtocolError(String, error_status_hint~ : Int)
    RemoteProtocolError(String, error_status_hint~ : Int)
    }

    Exception indicating a violation of the HTTP/1.1 protocol.

    • LocalProtocolError indicates that you tried to do something that HTTP/1.1 says is illegal (raised by Connection::send and the event constructors).
    • RemoteProtocolError indicates that the remote peer tried to do something that HTTP/1.1 says is illegal (raised by Connection::next_event).

    error_status_hint gives a suggestion as to what status code a server might use if this error occurred as part of a request. The default is 400 Bad Request.

    ProtocolError::error_status_hint

    fn ProtocolError::error_status_hint(self : ProtocolError) -> Int

    The suggested HTTP status code for responding to this error.

    ProtocolError::is_local

    fn ProtocolError::is_local(self : ProtocolError) -> Bool

    Whether this is a LocalProtocolError.

    ProtocolError::is_remote

    fn ProtocolError::is_remote(self : ProtocolError) -> Bool

    Whether this is a RemoteProtocolError.

    ProtocolError::message

    fn ProtocolError::message(self : ProtocolError) -> String

    The human readable message carried by this error.

    RuntimeError

    pub(all) suberror RuntimeError {
    RuntimeError(String)
    }

    Raised when the caller misuses the API in a way that is not an HTTP protocol violation, e.g. feeding more data after signalling EOF.

    Connection

    pub struct Connection {
    // private fields
    }

    An object encapsulating the state of an HTTP connection.

    It performs no I/O: feed received bytes with receive_data, pull parsed events with next_event, and turn your own events into bytes with send.

    Connection::client_is_waiting_for_100_continue

    fn Connection::client_is_waiting_for_100_continue(self : Connection) -> Bool

    Whether the client has sent Expect: 100-continue and is still waiting for a response.

    Connection::new

    fn Connection::new(our_role : Role, max_incomplete_event_size? : Int) -> Connection

    Create a connection playing our_role.

    max_incomplete_event_size is the maximum number of bytes we're willing to buffer of an incomplete event. In practice this mostly sets a limit on the maximum size of the request/response line + headers. If this is exceeded, then next_event will raise RemoteProtocolError.

    Connection::next_event

    fn Connection::next_event(self : Connection) -> NextEvent raise ProtocolError

    Parse the next event out of our receive buffer, update our internal state, and return it.

    This is a mutating operation -- think of it like calling next on an iterator. Returns one of:

    1. Event(event): an event object.
    2. NeedData: you need to read more data from your socket and pass it to receive_data before this method will be able to return any more events.
    3. Paused: we are not in a state where we can process incoming data (usually because the peer has finished their part of the current request/response cycle, and you have not yet called start_next_cycle).

    Raises RemoteProtocolError if the peer has misbehaved. You should close the connection (possibly after sending some kind of 4xx response).

    Once this method returns ConnectionClosed once, then all subsequent calls will also return ConnectionClosed.

    If this method raises then it also sets their_state to Error.

    Connection::our_role

    fn Connection::our_role(self : Connection) -> Role

    The role we are playing.

    Connection::our_state

    fn Connection::our_state(self : Connection) -> State

    The current state of whichever role we are playing.

    Connection::receive_data

    fn Connection::receive_data(self : Connection, data : BytesView) -> Unit raise RuntimeError

    Add data to our internal receive buffer.

    This does not actually do any processing on the data, just stores it. To trigger processing, you have to call next_event.

    Special case: if data is empty, then this indicates that the remote side has closed the connection (end of file). Calling receive_data(b"") multiple times is fine, and equivalent to calling it once.

    Raises RuntimeError if you pass an empty data, indicating EOF, and then pass a non-empty data, indicating more data that somehow arrived after the EOF.

    Connection::send

    fn Connection::send(self : Connection, event : Event) -> Bytes? raise ProtocolError

    Convert a high-level event into bytes that can be sent to the peer, while updating our internal state machine.

    Returns None if event is ConnectionClosed, and the bytes to send otherwise.

    Raises LocalProtocolError if sending this event at this time would violate our understanding of the HTTP/1.1 protocol. If this method raises then it also sets our_state to Error.

    Connection::send_failed

    fn Connection::send_failed(self : Connection) -> Unit

    Notify the state machine that we failed to send the data it gave us.

    This causes our_state to immediately become Error.

    Connection::send_with_data_passthrough

    fn Connection::send_with_data_passthrough(self : Connection, event : Event) -> Array[Bytes]? raise ProtocolError

    Identical to send, except that in situations where send returns a single byte string, this instead returns a list of them -- and when sending a Data event, this list is guaranteed to contain the exact Bytes object you passed in as Data.data.

    Connection::start_next_cycle

    fn Connection::start_next_cycle(self : Connection) -> Unit raise ProtocolError

    Attempt to reset our connection state for a new request/response cycle.

    If both client and server are in Done state, then resets them both to Idle in preparation for a new request/response cycle on this same connection. Otherwise, raises a LocalProtocolError.

    Connection::states

    fn Connection::states(self : Connection) -> States

    The current state of both the client and the server.

    Connection::their_http_version

    fn Connection::their_http_version(self : Connection) -> Bytes?

    The HTTP version our peer used in their last request or response, if any has been received.

    Connection::their_role

    fn Connection::their_role(self : Connection) -> Role

    The role our peer is playing.

    Connection::their_state

    fn Connection::their_state(self : Connection) -> State

    The current state of whichever role we are NOT playing.

    Connection::they_are_waiting_for_100_continue

    fn Connection::they_are_waiting_for_100_continue(self : Connection) -> Bool

    Whether our peer is a client waiting for a 100 Continue.

    Connection::trailing_data

    fn Connection::trailing_data(self : Connection) -> (Bytes, Bool)

    Data that has been received, but not yet processed, together with a flag that is true if the receive connection was closed.

    See the h11 docs on switching protocols for why you'd want this.

    Data

    pub struct Data {
    data : Bytes
    chunk_start : Bool
    chunk_end : Bool
    } derive(Eq)

    Part of an HTTP message body.

    chunk_start and chunk_end mark whether this data is from the start / the end of a chunk in chunked transfer encoding. They are only meaningful on events returned by Connection::next_event and are ignored by Connection::send. You probably shouldn't rely on them at all.

    Data::new

    fn Data::new(data : Bytes, chunk_start? : Bool, chunk_end? : Bool) -> Data

    EndOfMessage

    pub struct EndOfMessage {
    headers : Headers
    } derive(Eq)

    The end of an HTTP message. headers holds any trailing headers, which must be empty unless Transfer-Encoding: chunked is in use.

    EndOfMessage::new

    fn EndOfMessage::new(headers? : ArrayView[(Bytes, Bytes)]) -> EndOfMessage raise ProtocolError

    Event

    pub(all) enum Event {
    Request(Request)
    InformationalResponse(InformationalResponse)
    Response(Response)
    Data(Data)
    EndOfMessage(EndOfMessage)
    ConnectionClosed
    } derive(Eq,
    Debug
    )

    An h11 event. ConnectionClosed indicates that the sender has closed their outgoing connection (which does not necessarily mean that they can't receive further data).

    Headers

    pub struct Headers {
    // private fields
    }

    A list-like collection of headers. Iterating yields (lowercased_name, value) pairs; raw_items yields the names with their original casing.
    impl Eq for Headers

    Headers::at

    #alias("_[_]")
    fn Headers::at(self : Headers, idx : Int) -> (Bytes, Bytes)

    The idx-th header as a (lowercased_name, value) pair.

    Headers::empty

    fn Headers::empty() -> Headers

    An empty header list.

    Headers::get

    fn Headers::get(self : Headers, idx : Int) -> (Bytes, Bytes)?

    The idx-th header as a (lowercased_name, value) pair, or None if idx is out of range.

    Headers::is_empty

    fn Headers::is_empty(self : Headers) -> Bool

    Headers::iter

    fn Headers::iter(self : Headers) -> Iter[(Bytes, Bytes)]

    Iterate over (lowercased_name, value) pairs.

    Headers::length

    fn Headers::length(self : Headers) -> Int

    Headers::new

    fn Headers::new(items : ArrayView[(Bytes, Bytes)]) -> Headers raise ProtocolError

    Normalize and validate a list of (name, value) pairs.

    Header names must be tokens and values must be valid field values (no leading/trailing whitespace, NUL, CR or LF). Content-Length and Transfer-Encoding receive extra checks.

    Headers::raw_items

    fn Headers::raw_items(self : Headers) -> Array[(Bytes, Bytes)]

    All headers as (raw_name, value) pairs, preserving the original casing of the names.

    Headers::to_array

    fn Headers::to_array(self : Headers) -> Array[(Bytes, Bytes)]

    All headers as (lowercased_name, value) pairs.

    InformationalResponse

    pub struct InformationalResponse {
    status_code : Int
    headers : Headers
    http_version : Bytes
    reason : Bytes
    } derive(Eq)

    An HTTP informational response; status_code is always in the range [100, 200).

    InformationalResponse::new

    fn InformationalResponse::new(status_code~ : Int, headers~ : ArrayView[(Bytes, Bytes)], http_version? : Bytes, reason? : Bytes) -> InformationalResponse raise ProtocolError

    Construct and validate an InformationalResponse.

    NextEvent

    pub(all) enum NextEvent {
    Event(Event)
    NeedData
    Paused
    } derive(Eq,
    Debug
    )

    The result of Connection::next_event.

    Request

    pub struct Request {
    method_ : Bytes
    target : Bytes
    headers : Headers
    http_version : Bytes
    } derive(Eq)

    The beginning of an HTTP request.

    • method: an HTTP method, e.g. b"GET" or b"POST".
    • target: the target of the request, e.g. b"/index.html", or one of the more exotic formats described in RFC 7230 section 5.3.
    • headers: request headers, see Headers.
    • http_version: the protocol version, e.g. b"1.1".

    Request::new

    fn Request::new(method_~ : Bytes, target~ : Bytes, headers~ : ArrayView[(Bytes, Bytes)], http_version? : Bytes) -> Request raise ProtocolError

    Construct and validate a Request.

    Raises LocalProtocolError if the method or target contain illegal characters, if the headers are invalid, if an HTTP/1.1 request lacks a Host header, or if there are multiple Host headers.

    test {
    let req = @h11.Request::new(method_=b"GET", target=b"/", headers=[
    (b"Host", b"example.com"),
    ])
    inspect(req.headers.length(), content="1")
    }

    Response

    pub struct Response {
    status_code : Int
    headers : Headers
    http_version : Bytes
    reason : Bytes
    } derive(Eq)

    The beginning of an HTTP response; status_code is always in the range [200, 1000).

    Response::new

    fn Response::new(status_code~ : Int, headers~ : ArrayView[(Bytes, Bytes)], http_version? : Bytes, reason? : Bytes) -> Response raise ProtocolError

    Construct and validate a Response.

    test {
    let resp = @h11.Response::new(status_code=200, headers=[], reason=b"OK")
    inspect(resp.status_code, content="200")
    }

    Role

    pub(all) enum Role {
    Client
    Server
    } derive(Eq, Hash,
    Debug
    )

    The two roles in an HTTP conversation.
    impl Show for Role

    Role::other

    fn Role::other(self : Role) -> Role

    The other role.

    State

    pub(all) enum State {
    Idle
    SendResponse
    SendBody
    Done
    MustClose
    Closed
    MightSwitchProtocol
    SwitchedProtocol
    Error
    } derive(Eq, Hash,
    Debug
    )

    The states of the client and server state machines. See the h11 documentation for the full diagrams.
    impl Show for State

    States

    pub(all) struct States {
    client : State
    server : State
    } derive(Eq, Hash,
    Debug
    )

    The joint state of both parties.
    impl Show for States

    States::get

    #alias("_[_]")
    fn States::get(self : States, role : Role) -> State

    The state of role.

    DEFAULT_MAX_INCOMPLETE_EVENT_SIZE

    let DEFAULT_MAX_INCOMPLETE_EVENT_SIZE : Int

    If we ever have this much buffered without it making a complete parseable event, we error out. The only time we really buffer is when reading the request/response line + headers together, so this is effectively the limit on the size of that.

    get_comma_header

    fn get_comma_header(headers : Headers, name : Bytes) -> Array[Bytes]

    Collect the comma-separated, case-insensitive values of every header named name (which must be lowercase).

    This naive splitting is wrong for headers that allow quoted strings, but fine for the headers we use it on: Connection, Content-Length, Transfer-Encoding (we reject anything but "chunked" anyway) and Expect.

    has_expect_100_continue

    fn has_expect_100_continue(request : Request) -> Bool

    Whether a request carries Expect: 100-continue (ignored for HTTP/1.0, as required by RFC 7231 section 5.1.1).

    set_comma_header

    fn set_comma_header(headers : Headers, name : Bytes, new_values : ArrayView[Bytes]) -> Headers raise ProtocolError

    Return a copy of headers where every header named name (lowercase) is replaced by one header per entry of new_values, appended at the end with a title-cased name.