#gaato/http/h1server

    An HTTP/1.1 server connection engine over the gaato/http/rt ports. serve_conn serves the requests of one accepted connection with an @http.Handler. Every head, chunk-size line and trailer section is read through the bounded gaato/http/h1 codec. Head and body operations have deadlines; a handler deadline is optional.

    Experimental. The engine's settings and its reporting may still change; an importer lists it in options("allow-experimental-packages": ["gaato/http/h1server"]). gaato/http-async wires it to sockets, TLS and an event loop; most applications use that instead.

    #Serving a connection

    The engine needs a connection and the ports only. Here the in-memory doubles of gaato/http/sim stand in for a socket, so the example runs on every target:

    ///|
    test "serve a request" {
    let driver = @driver.Driver::new()
    let coordination = @sim.SimCoordination::new(driver)
    let (client, server) = @sim.pair(coordination)
    let errors = []
    driver.run(() => {
    let task = driver.spawn(() => {
    @h1server.serve_conn(
    server,
    request => {
    let greeting = "hello from " + request.url
    @http.Response::new(200, body=Bytes(@http.encode_utf8(greeting)))
    },
    clock=@sim.SimClock::new(driver),
    timer=@sim.SimTimer::new(driver),
    coordination~,
    scheme="https",
    on_error=error => errors.push(error),
    )
    })
    client.write(b"GET /docs HTTP/1.1\r\nHost: example.test\r\n\r\n")
    // A head the codec refuses is answered and ends the connection.
    client.write(b"GET / HTTP/1.1\r\nHost: a\r\nHost: b\r\n\r\n")
    let out = Buffer()
    while client.read(max_len=4096) is Some(bytes) {
    out.write_bytes(bytes)
    }
    let text = @utf8.decode(out.to_bytes())
    assert_true(text.has_prefix("HTTP/1.1 200 OK\r\n"))
    assert_true(text.contains("hello from https://example.test/docs"))
    assert_true(text.contains("HTTP/1.1 400 Bad Request\r\n"))
    ignore(task.wait())
    })
    assert_eq(server.closes(), 1)
    assert_eq(errors.length(), 1)
    }

    #What it guarantees

    • Ownership. serve_conn owns its connection from the start and closes it on every exit, cancellation and refused settings included.
    • Strict framing. Request smuggling (Content-Length with Transfer-Encoding, a duplicate or invalid Content-Length, Transfer-Encoding on HTTP/1.0) is answered with 400 and the connection closes, so no byte after the head is read as another request. Other heads are refused with 431 (past the head limits), 400 (malformed, a missing or duplicate Host), 501 (CONNECT, a transfer coding other than chunked), 505 (an HTTP version other than 1.x) or 417 (an expectation other than 100-continue).
    • Bounded input. Heads within max_header_bytes and 100 fields, chunk lines within 4 KiB, trailers within 16 KiB and 100 fields, bodies within max_body_bytes (413), reads of at most 64 KiB, and at most 64 KiB of raw read-ahead while a body is unclaimed. Its first pull cancels and joins the monitor's read before taking over, consuming buffered bytes first; there is always one connection reader. After the body completes the monitor makes at most one read ahead for the next head. A body left unread is drained up to drain_max_bytes, otherwise the connection closes.
    • Deadlines. header_timeout_ms from the end of one exchange to the end of the next head (408 once a byte arrived, a silent close before), body_timeout_ms for a whole request body (408) and for each write and each read of a streamed response, handler_timeout_ms for the handler (504). A timeout before the response head is 408 only when a request-body pull was in progress or already failed; an independent response producer timing out becomes 500 and reaches on_error, even with an unread body. A response stream that pauses longer than body_timeout_ms ends its connection, so long-polling handlers raise that limit.
    • Failures. Before a response head is written, a body failure replaces the handler's response with its own status, even when the handler caught it. An invalid response or a producer failure before the head becomes 500; failures after the head close the connection. Failures reach on_error as an @http.TransportError. @http.Handler is noraise; applications handle their own exceptions before returning a response.
    • Peers. 100 Continue is sent when the handler first reads the body. A read failure while the handler runs cancels it, including while its body is unclaimed and the read-ahead buffer has room. Once that buffer fills, monitoring stops until the handler reads; body pulls and a configured handler deadline must provide progress. The handler deadline defaults to None, so a handler that never pulls or returns can still wait indefinitely at this bound. Read-ahead neither sends 100 Continue nor starts the lazy body deadline. EOF does not cancel the handler, because the peer may have closed only its sending half. HTTP/1.0 keeps a connection only when it asks for keep-alive and the response has a length; it is never sent a chunked body.
    • Raw bytes. The engine adds the framing fields and Connection and nothing else; content codings are a handler wrapper's job.
    • Listener boundary. The caller accepts connections and owns the background scheduler. max_connections (256) and shutdown drain_ms (10 seconds) are validated settings for that adapter, which enforces them across connections. serve_conn checks stopping between requests and before writing a response to send Connection: close; cancelling it unwinds and closes its connection. It does not bind, accept or manage detached jobs.

    ServerConfigError

    pub(all) suberror ServerConfigError {
    InvalidMaxBodyBytes(value~ : Int)
    InvalidBodyTimeoutMs(value~ : Int)
    InvalidHeaderTimeoutMs(value~ : Int)
    InvalidMaxConnections(value~ : Int)
    InvalidMaxHeaderBytes(value~ : Int)
    InvalidDrainMaxBytes(value~ : Int)
    InvalidHandlerTimeoutMs(value~ : Int)
    InvalidDrainMs(value~ : Int)
    } derive(
    Debug
    )

    Settings refused before any connection is served.

    ServerConfigError::to_repr

    Formats the refused setting for diagnostics.

    ServerLimits

    pub(all) struct ServerLimits {
    max_body_bytes : Int
    header_timeout_ms : Int
    body_timeout_ms : Int
    max_header_bytes : Int
    max_connections : Int
    drain_max_bytes : Int
    handler_timeout_ms : Int?
    drain_ms : Int
    } derive(Eq,
    Debug
    )

    The bounds a server applies to each connection and request. Build them from ServerLimits::default() with a record update; validate refuses values that would disable a bound.

    test {
    let limits = { ..@h1server.ServerLimits::default(), max_body_bytes: 4096, }
    limits.validate()
    assert_eq(limits.max_header_bytes, 16384)
    }

    ServerLimits::default

    fn ServerLimits::default() -> ServerLimits

    1 MiB bodies, five-second header and body budgets, 16 KiB heads, 256 connections, 64 KiB of unread body drained, no handler deadline and ten seconds to drain at shutdown.

    ServerLimits::equal

    Compares limits field by field.

    ServerLimits::not_equal

    fn ServerLimits::not_equal(x : ServerLimits, y : ServerLimits) -> Bool

    Compares limits field by field.

    ServerLimits::to_repr

    Formats limits for diagnostics.

    ServerLimits::validate

    fn ServerLimits::validate(self : ServerLimits) -> Unit raise ServerConfigError

    Refuses limits that would disable a bound, naming the first invalid field.

    serve_conn

    async fn serve_conn(conn : &
    Conn
    , handler : async (
    Request
    ) ->
    Response
    noraise, clock~ : &
    Clock
    , timer~ : &
    Timer
    , coordination~ : &
    Coordination
    , scheme~ : String, limits? : ServerLimits, peer? : String, authority? : String, on_error? : (
    TransportError
    ) -> Unit, stopping? : () -> Bool, background? : (async () -> Unit) -> Unit) -> Unit raise ServerConfigError

    Serves HTTP/1.1 (and 1.0) requests on one accepted connection until it ends, and closes it on every exit, cancellation and invalid limits included. conn belongs to this call from the start.

    Each request reaches handler with an absolute URL made of scheme (http or https, the scheme the connection was accepted under), the Host field and the target; an HTTP/1.0 request without Host uses authority. With peer, the request carries it as @http.remote_addr; with background, as @http.background, so the handler can start work that outlives the response on the caller's scheduler. Without one, no background attribute is advertised.

    The engine reads heads, chunk-size lines and trailers through the bounded gaato/http/h1 codec and applies limits (see ServerLimits). A head the codec refuses is answered and the connection closed: 431 for a head past max_header_bytes or its field limit, 400 for malformed heads and request smuggling (Content-Length with Transfer-Encoding, a duplicate Content-Length), 501 for an unsupported transfer coding and for CONNECT, 505 for an HTTP version other than 1.x, 417 for an expectation other than 100-continue, 413 for a Content-Length past max_body_bytes and 408 when the head does not arrive within header_timeout_ms. A body that fails while the handler runs replaces its response with 413, 408 or 400; a handler past handler_timeout_ms is cancelled and answered with 504; a response the engine cannot write (a status outside 200 to 599, an invalid field, a stream that fails at its first read) becomes 500. Each of these is reported to on_error as a TransportError.

    100 Continue is sent when the handler first reads the body, never before, so a handler that refuses the request does not invite the body. A read failure on the connection while the handler runs cancels it; EOF does not, since the peer may have closed only its sending half. After a response, a body the handler left unread is drained up to drain_max_bytes, otherwise the connection closes. stopping is asked before each request; once it returns true the current response carries Connection: close.

    Raises ServerConfigError for invalid limits, before reading anything; every other failure (including a scheme other than http or https) goes to on_error.

    test {
    let driver = @driver.Driver::new()
    let coordination = @sim.SimCoordination::new(driver)
    let (client, server) = @sim.pair(coordination)
    driver.run(() => {
    let task = driver.spawn(() => {
    @h1server.serve_conn(
    server,
    request => {
    @http.Response::new(200, body=Bytes(@http.encode_utf8(request.url)))
    },
    clock=@sim.SimClock::new(driver),
    timer=@sim.SimTimer::new(driver),
    coordination~,
    scheme="http",
    )
    })
    client.write(
    b"GET /hello HTTP/1.1\r\nHost: example.test\r\nConnection: close\r\n\r\n",
    )
    let out = Buffer()
    while client.read(max_len=4096) is Some(bytes) {
    out.write_bytes(bytes)
    }
    assert_true(out.to_bytes().has_suffix(b"http://example.test/hello"))
    ignore(task.wait())
    })
    assert_eq(server.closes(), 1)
    }