//! An HTTP/1.1 server: requests read and parsed, responses built and written,
//! and a router that serves GET routes.
//!
//! `Server` is the simple way in: give it a handler for each path with `get`,
//! then `listen` on an address. A handler is a plain function from a
//! `Request` to a `Response`, so it cannot await anything. `read_request`,
//! `write_response` and `parse_request_head` are the parts underneath, for
//! serving a `std/net` `TcpStream` yourself. There is no HTTP client. Every
//! module sees these names without an import, and a module's own definition
//! of one of them shadows the one here.
//!
//! Only `Content-Length` says how long a request's body is: chunked transfer
//! encoding is not supported, and a chunked request's body is misread as the
//! next request. A request's header names are lower-cased, since HTTP
//! ignores their case, and the order of a response's headers on the wire can
//! change from one run of a program to the next.
//!
//! `Server::listen` serves until the process is stopped, so this example
//! has the server `dispatch` a request parsed from bytes, with no socket:
//!
//! ```
//! let app = Server::new().get("/hello", |_| Response::text(200, "hi"))
//!
//! let raw = bytes_from_string("GET /hello HTTP/1.1\r\nHost: example\r\nX-Trace: abc\r\n\r\n")
//! let req = parse_request_head(raw, Limits::default()).unwrap().unwrap()
//! assert_eq(req.headers.get("x-trace").unwrap(), "abc") // names are lower-cased
//!
//! let resp = app.dispatch(req)
//! assert_eq(resp.status, 200)
//! assert_eq(resp.headers.get("content-length").unwrap(), "2")
//! let wire = resp.to_bytes().to_string().unwrap()
//! assert(wire.starts_with("HTTP/1.1 200 OK\r\n"), "the status line comes first")
//! assert(wire.ends_with("\r\n\r\nhi"), "then a blank line and the body")
//! ```
// Nova standard library -- HTTP/1.1, server side.
//
// Compiled as an implicit module and glob-imported into every user module, so
// these names need no `import` (see docs/adr/0004-stdlib-compile-model.md).
// The transport is `std/net`. Parsing and serialising never open a socket;
// `Server::listen`, at the end of this file, binds one and is the only code
// here that does.
//
// **What ships, against `nova-spec/20-STDLIB.md` section 6.** That section
// specifies a router over `pub type Handler = async fn(Request) -> Response`.
// That alias does not parse -- measured, `P0001: expected type (in type
// alias), found async` -- and a type alias of any kind is `E0900`, so
// `Server` at the end of this file types each handler inline as
// `fn(Request) -> Response`. A handler is therefore synchronous: it builds
// its response without awaiting. Only GET routes ship, and there is no
// client. `Server::listen` shuts down gracefully on SIGTERM or SIGINT
// (Ctrl+Break or Ctrl+C on Windows); see its own comment and
// docs/adr/0022-process-shutdown-signals.md. See also
// docs/adr/0019-offset-table-intrinsic-boundary.md and section 6's own dated
// amendments.
//
// **Parsing is one intrinsic and it returns offsets, not values.**
// `http_parse_request` hands back a flat `[Int]` table of byte offsets into
// the caller's own buffer -- no copies, no Rust-side state, nothing to
// release. `parse_offsets` below is a thin `pub` wrapper over it, the same
// shape `std/bytes`'s `bytes_from_ints` uses, so a fixture can pin the raw
// encoding; ordinary callers want `parse_request_head`.
//
// **Header names are lower-cased and the original casing is not kept.** HTTP
// field names are case-insensitive, so a `Map` keyed on the raw casing would
// answer `get("content-length")` differently depending on what the peer sent.
//
// **Two limitations of `read_request` below share one cause: it reads
// exactly one request's worth of bytes per connection turn and trusts
// `Content-Length` to say where that ends.** `Server` shares the first and
// not the second: it keeps one buffer per connection, so it answers
// pipelined requests in order.
//
// - `Content-Length` is the only framing either understands. Chunked
// transfer-encoding is neither supported nor detected, so a chunked
// request's body is left on the connection and is misread as the next
// request.
// - `read_request` discards bytes already read past the current request's
// body, so a pipelined request is lost and its connection deadlocks rather
// than merely going unsupported. See `read_request`'s own doc comment for
// the exact wording this needs.
//
// **A third limitation, with a different cause.** Neither `conn.read` call
// in `read_request` below carries a timeout, so a peer that connects and
// then sends nothing -- or sends a partial head or body and stops -- holds
// the task serving it parked forever. `std/net::TcpStream::read_timeout`
// already exists (`std/net/lib.nova`) and was not used there; see
// `read_request`'s own doc comment for the reasoning, and
// docs/adr/0019-offset-table-intrinsic-boundary.md's Consequences section
// for the same gap recorded against that ADR. `Server` does not share this
// one either: every wait in its `serve` is bounded.
// The request method. `Unknown` carries the raw token rather than discarding
// it: a request may carry any method a peer invents, and the parser must not
// abort on one it does not know.
//
// Named `Unknown` rather than `nova-spec/20-STDLIB.md` section 6's `Other`,
// deliberately. `Other` is already an `IoErrorKind` variant in `std/io`, every
// std module is glob-imported into every module, and whether two std modules
// may export one variant name is not established on this tree. This increment
// is not the place to find out.
/// An HTTP request method.
///
/// Seven methods have variants of their own, each matched exactly as written
/// in capitals; any other method, `get` in lower case included, is `Unknown`.
pub type Method =
/// `GET`.
| Get
/// `POST`.
| Post
/// `PUT`.
| Put
/// `DELETE`.
| Delete
/// `PATCH`.
| Patch
/// `HEAD`.
| Head
/// `OPTIONS`.
| Options
/// Any other method, holding its name as the request wrote it.
| Unknown(String)
// Why a request head was rejected.
//
// **This type is one half of a wire contract, for its first seven kinds.**
// The other half is the `ERR_*` constants in
// `crates/nova-runtime/src/http.rs`, which produce those seven codes. The two
// are independent copies of one numbering -- the shape that has already
// produced a miscompile in this project -- so `tests/runtime/http_malformed.nova`
// pins together the ones `http_error_kind_of` actually maps; see that
// function's own doc comment for exactly which those are, since "per kind"
// would overclaim -- not every one of the seven is currently reachable from
// a fixture through this type.
//
// `InvalidUtf8`, `BodyTooLarge` and `Transport` have no runtime counterpart
// and no `ERR_*` code: `std/http` reports all three itself, so none of them
// needs to stay in sync with the intrinsic's own numbering the way the first
// seven do.
/// Why reading or parsing a request failed.
pub type HttpErrorKind =
/// The request head is too long: longer than the `max_head_bytes` of the
/// `Limits` given, or than 8,192 bytes, whatever the limit.
| HeadTooLarge
/// The request head has more headers than the `max_header_count` of the
/// `Limits` given, or more than 100, whatever the limit.
| TooManyHeaders
/// The request line is malformed: its method, target or version is not
/// valid, or the version is not `HTTP/1.0` or `HTTP/1.1`.
| BadRequestLine
/// A header line is malformed, or `Content-Length` appears twice or is
/// not a whole number written in digits.
| BadHeader
/// A line ends badly, as with a carriage return not followed by a line
/// feed.
| BadNewline
/// The parser finished a head without its method, target or version,
/// which it is not expected to do.
| IncompleteHeadFields
/// Part of a parsed head lay outside the bytes given, which the parser
/// is not expected to do.
| SliceOutOfBuffer
/// A header's value is not valid UTF-8.
| InvalidUtf8
/// `Content-Length` is more than the `max_body_bytes` of the `Limits`
/// given.
| BodyTooLarge
/// Reading from the connection failed, or the connection closed before
/// the request was complete.
| Transport
/// The error `read_request` and `parse_request_head` return.
pub record HttpError {
/// What went wrong.
pub kind: HttpErrorKind,
/// A short description, such as `duplicate Content-Length header`. For a
/// `Transport` error it is the connection's own error message, or
/// `connection closed mid-head` or `connection closed mid-body`.
pub message: String,
}
// Map a parse status to its kind. The argument is the **negated** status the
// intrinsic returned, so a status of `-3` arrives here as `3`.
//
// An unrecognised code is `BadRequestLine` rather than a panic: a status this
// function does not know is a runtime bug, and mapping it to a diagnosable
// kind keeps an error flowing instead of ending the process -- the same call
// `std/io`'s `io_error_kind_of` makes for the same reason.
//
// **Which of the seven codes a Nova fixture actually drives through this
// function, and which do not.** `BadRequestLine`, `BadHeader` and
// `BadNewline` (3, 4, 5) do: `tests/runtime/http_malformed.nova` feeds each
// one's triggering input to `parse_request_head` under `Limits::default()`
// and asserts the mapped kind on the `Err` it returns. `HeadTooLarge` and
// `TooManyHeaders` (1, 2) are different: their own raw negative status is
// observed directly through `parse_offsets` in `tests/runtime/http_limits.nova`,
// but every `parse_request_head` call in that fixture supplies a `Limits` at
// or below the runtime's own ceiling, so `parse_request_head`'s two
// Nova-side checks (just below) return a literal `HeadTooLarge` or
// `TooManyHeaders` before ever reaching `status < 0` there -- this
// function's own arms for 1 and 2 are real code, reachable by a caller whose
// `Limits` is looser than the runtime's wall, but no fixture currently
// drives them. `IncompleteHeadFields` (6) and `SliceOutOfBuffer` (7) are
// undemonstrated by any fixture, and differ from each other at their Rust
// call sites: `SliceOutOfBuffer`'s (`offset_within` in
// `crates/nova-runtime/src/http.rs`) argues from httparse's zero-copy
// contract that it cannot fire in practice and checks anyway, while
// `IncompleteHeadFields`'s call site carries no such argument either way.
/// Returns the `HttpErrorKind` for an error code from `parse_offsets`, which
/// reports the code as a negative status: pass the status negated.
///
/// Codes 1 to 7 are `HeadTooLarge`, `TooManyHeaders`, `BadRequestLine`,
/// `BadHeader`, `BadNewline`, `IncompleteHeadFields` and `SliceOutOfBuffer`,
/// in that order. Any other code gives `BadRequestLine`.
pub fn http_error_kind_of(code: Int) -> HttpErrorKind {
if code == 1 { return HeadTooLarge }
if code == 2 { return TooManyHeaders }
if code == 3 { return BadRequestLine }
if code == 4 { return BadHeader }
if code == 5 { return BadNewline }
if code == 6 { return IncompleteHeadFields }
if code == 7 { return SliceOutOfBuffer }
BadRequestLine
}
// Caller-chosen ceilings.
//
// **`max_head_bytes` and `max_header_count` may only tighten, never loosen.**
// The runtime compiles in its own hard wall (8 KiB and 100, in
// `crates/nova-runtime/src/http.rs`) and checks it before allocating anything
// on the Nova side, because the intrinsic takes one argument and there is
// nowhere for a caller's limit to arrive. The two fields here are checked on
// this side, in addition. `max_body_bytes` is this side's alone: the intrinsic
// returns `body_start` and never looks at the body.
/// Limits on the size of a request, for `read_request` and
/// `parse_request_head`.
///
/// `Limits::default()` allows a head of 8,192 bytes with 100 headers, and a
/// body of 1,048,576 bytes (1 MiB); `Server` always uses those. The two head
/// limits can only be made stricter: a head over 8,192 bytes, or with more
/// than 100 headers, is refused whatever they say.
pub record Limits {
/// The most bytes a request head may take.
pub max_head_bytes: Int,
/// The most headers a request may have.
pub max_header_count: Int,
/// The largest `Content-Length` accepted.
pub max_body_bytes: Int,
}
impl Default for Limits {
fn default() -> Limits {
Limits { max_head_bytes: 8192, max_header_count: 100, max_body_bytes: 1048576 }
}
}
// A parsed request. `body` is the bytes after the head, already bounded by
// `Content-Length` and by `Limits::max_body_bytes`.
/// An HTTP request: its method, target, headers and body.
pub record Request {
/// The request's method.
pub method: Method,
/// The request's target as the request wrote it, query string included,
/// such as `/search?q=nova`. Nothing in it is decoded.
pub path: String,
/// The headers, by lower-cased name. Each value has the spaces and tabs
/// around it removed. Of two headers with the same name, the last is
/// kept.
pub headers: Map<String, String>,
/// The body: as many bytes as `Content-Length` says, or none without that
/// header. Empty in a request from `parse_request_head`.
pub body: Bytes,
}
// The raw offset table for `buf`, exactly as
// `crates/nova-runtime/src/http.rs` encodes it.
//
// **Element 0 is a status and a caller must read it first.** `0` is a complete
// head, `1` is a partial one (read more bytes and call again), and a negative
// value is the negation of an error kind. On any non-zero status **the array
// has length 1**, so a caller who skips the check indexes out of bounds rather
// than reading a plausible-looking offset. That is deliberate.
//
// Exposed so `tests/runtime/http_offsets.nova` can pin the encoding from this
// side; ordinary callers want `parse_request_head`.
/// Returns the raw table of byte offsets that parsing the request head at the
/// start of `buf` gives. `parse_request_head` is usually the one to call.
///
/// Element 0 is a status: 0 for a complete head, 1 for an incomplete one, and
/// otherwise a negative error code, which `http_error_kind_of` turns into a
/// kind once negated. Unless the status is 0, the table holds only that
/// element. A complete head's table goes on with the method's start
/// and length, the target's start and length, the HTTP minor version (0 or
/// 1), the number of headers, four elements per header (its name's start and
/// length, then its value's), and last the offset where the body starts. All
/// offsets count bytes from the start of `buf`.
pub fn parse_offsets(buf: Bytes) -> [Int] { http_parse_request(buf) }
// Decode a complete head out of `buf`, or report that more bytes are needed.
//
// `Ok(None)` means the head is incomplete: read more and call again with the
// longer buffer. `Ok(Some(req))` is a complete head, with `body` empty --
// `read_request` fills that in once it knows `Content-Length`.
/// Parses the request head at the start of `buf`.
///
/// Returns `Ok(None)` if `buf` does not yet hold a whole head: read more and
/// call again with the longer buffer. Once it does, returns `Ok(Some(req))`,
/// whose `body` is empty: the body's length is in its `content-length`
/// header. Lines may end with a carriage return and line feed or with a line
/// feed alone, and empty lines before the request line are skipped.
///
/// Returns an `HttpError` whose kind says how the head is malformed, or
/// `HeadTooLarge` or `TooManyHeaders` if it breaks `limits`. The byte limit
/// applies to all of `buf`, so bytes after the head count against it too. A
/// second `Content-Length` header is `BadHeader`, and a header value that is
/// not valid UTF-8 is `InvalidUtf8`.
///
/// Part of a head, then all of it:
///
/// ```
/// let limits = Limits::default()
///
/// // Part of a head is `Ok(None)`: read more, then try again with it all.
/// let start = bytes_from_string("GET /items HTTP/1.1\r\nHost: shop")
/// assert(parse_request_head(start, limits).unwrap().is_none(), "not a whole head yet")
/// let whole = start.concat(bytes_from_string("\r\n\r\n"))
/// let req = parse_request_head(whole, limits).unwrap().unwrap()
/// assert_eq(req.path, "/items")
/// assert_eq(req.headers.get("host").unwrap(), "shop")
///
/// // A malformed head is an `Err`.
/// assert(
/// parse_request_head(bytes_from_string("HELLO\r\n\r\n"), limits).is_err(),
/// "no target or version",
/// )
/// ```
pub fn parse_request_head(buf: Bytes, limits: Limits) -> Result<Option<Request>, HttpError> {
// Measures the accumulated read buffer, not the head's own exact length:
// when called from `read_request`, `buf` may hold a few bytes past the
// head's end if the read that completed it also pulled in the start of
// the body, so a legal head can be rejected slightly before its own
// exact byte count if the peer's writes happen to land that way. The
// design spec's own words license exactly this ("bounds the buffer a
// client can force before the head completes"), not an exact bound on
// the head alone.
if buf.len() > limits.max_head_bytes {
return Err(HttpError { kind: HeadTooLarge, message: "request head over max_head_bytes" })
}
let t = parse_offsets(buf)
let status = t[0]
if status == 1 { return Ok(None) }
if status < 0 {
let kind = http_error_kind_of(0 - status)
return Err(HttpError { kind: kind, message: "request head rejected by the parser" })
}
let n = t[6]
if n > limits.max_header_count {
return Err(
HttpError { kind: TooManyHeaders, message: "header count over max_header_count" },
)
}
let method = method_from(text_at(buf, t[1], t[2]))
let path = text_at(buf, t[3], t[4])
let mut headers: Map<String, String> = Map::new()
let mut i = 0
while i < n {
let at = 7 + 4 * i
// Lower-cased because HTTP field names are case-insensitive. Full
// Unicode lowercasing has nothing to bite on here: `httparse`
// validates a name as an HTTP token, which is ASCII-only, so this
// sees only ASCII.
let name = text_at(buf, t[at], t[at + 1]).to_lower()
let mut value = ""
match text_at_utf8(buf, t[at + 2], t[at + 3]) {
Ok(s) => value = s
Err(e) => return Err(e)
}
// A second `Content-Length` header is rejected outright rather than
// silently overwritten by `insert` below. RFC 9112 section 6.3 treats
// multiple `Content-Length` fields with differing values as invalid
// framing; this takes the simpler and stricter position of rejecting
// any repeat, matching or not, rather than keeping the first value
// and comparing it against every later one -- there is exactly one
// value available to compare against here regardless, since this
// loop (like `Map::insert`) only ever sees the latest one once a
// name repeats, so "compare instead of reject" would need its own
// separate bookkeeping to buy anything.
if name == "content-length" && headers.contains_key(name) {
return Err(HttpError { kind: BadHeader, message: "duplicate Content-Length header" })
}
headers.insert(name, value)
i = i + 1
}
Ok(Some(Request { method: method, path: path, headers: headers, body: bytes_from_string("") }))
}
// The bytes `buf[start .. start+len]` as text, defaulting to the empty
// string if they are not valid UTF-8.
//
// Safe for the method, the path, and a header's name -- but not for a
// header's value, which is why that fourth call site uses `text_at_utf8`
// below instead of this function. Checked directly against the locked
// `httparse` 1.10.1 source (`crates/nova-runtime`'s `Cargo.lock` entry)
// rather than assumed:
//
// - The method is matched against literal `"GET "`/`"POST "` or, failing
// that, `parse_token`'s `is_method_token`, which is backed by `TOKEN_MAP`
// -- ASCII-only -- so it cannot be anything but ASCII.
// - A header's name is matched against `TOKEN_MAP` the same way
// (`is_header_name_token`), so the same argument applies.
// - The path looks like it should be the same risk as a header's value --
// `parse_uri` scans it against `URI_MAP`, which (like `HEADER_VALUE_MAP`
// below) admits `0x80..=0xFF` -- but `parse_uri` then validates the whole
// scanned slice with `str::from_utf8` and fails the *entire request line*
// (`Error::Token`) if that fails. So `httparse::Request::path` is
// `Option<&str>`, not bytes, and a `Complete` parse cannot have produced
// an invalid one. Measured, not assumed: a lone `0x80` byte placed in the
// path (no valid UTF-8 continuation) was tried against this parser
// directly, and it rejected the whole request line as `BadRequestLine`
// rather than completing with an undecodable path -- this function's
// `None` branch is not reachable from that call site.
fn text_at(buf: Bytes, start: Int, len: Int) -> String {
match buf.slice(start, start + len).to_string() {
Some(s) => s
None => ""
}
}
// The bytes `buf[start .. start+len]` as text, or `InvalidUtf8` if they are
// not valid UTF-8.
//
// Used only for a header's value -- the one call site `text_at`'s doc
// comment, just above, does not cover. `httparse::Header::value` is `&[u8]`,
// not `&str`: httparse's own doc comment on that field says a value "may
// not" be ASCII and stores it as bytes with no UTF-8 check anywhere, unlike
// the method, path and header-name sites, which are already `&str` by the
// time this module sees them. A byte outside ASCII is legal there under
// HTTP's obs-text allowance (`HEADER_VALUE_MAP` admits `0x80..=0xFF`, the
// same range `URI_MAP` admits), and such a byte need not be valid UTF-8 on
// its own -- measured directly: a request whose entire header value is one
// literal `0x80` byte parses to `status = 0` like any other request, with a
// value offset pointing at a byte `to_string` reports as not UTF-8.
// Silently substituting "" there would discard real request data with no
// signal, which is what an earlier version of this module did at every call
// site alike. See `tests/runtime/http_malformed.nova`'s non-UTF-8 case.
fn text_at_utf8(buf: Bytes, start: Int, len: Int) -> Result<String, HttpError> {
match buf.slice(start, start + len).to_string() {
Some(s) => Ok(s)
None => Err(HttpError { kind: InvalidUtf8, message: "header value is not valid UTF-8" })
}
}
fn method_from(token: String) -> Method {
if token == "GET" { return Get }
if token == "POST" { return Post }
if token == "PUT" { return Put }
if token == "DELETE" { return Delete }
if token == "PATCH" { return Patch }
if token == "HEAD" { return Head }
if token == "OPTIONS" { return Options }
Unknown(token)
}
// A response to serialise onto a connection.
//
// **Header order on the wire is `headers`' iteration order**, which is a
// `Map`'s bucket order and therefore varies per process: `impl Hash for
// String` is seeded per process, which is exactly the property that makes a
// `Map` keyed on attacker-supplied header names resist a precomputed
// collision set. HTTP does not require a header order, so nothing here
// depends on it -- but that same seeding means a whole-response `.eq()`
// against one fixed byte sequence cannot be a stable test, not merely an
// inconvenient one to write: which of two headers comes first changes from
// run to run. Measured, not just argued from the seeding --
// `tests/runtime/http_serialise.nova` compared the whole buffer against one
// fixed ordering first, and printed `wire matches false` about as often as
// `true` across repeated separate runs. It checks the parts a header order
// cannot move instead: the exact status line, the exact tail (the blank line
// and the body), and that each expected header line occurs somewhere
// between them.
/// An HTTP response: a status code, headers and a body.
///
/// `ok`, `text`, `json` and `not_found` build the common ones, and each sets
/// `content-length`. A response built by hand should set it too: `Server`
/// keeps the connection open after a response, so without it a client cannot
/// tell where the body ends.
pub record Response {
/// The status code, such as 200 or 404. It is written as given, without
/// a check.
pub status: Int,
/// The headers, by name. Each name is written as given, and their order
/// on the wire can change from one run of a program to the next.
pub headers: Map<String, String>,
/// The body.
pub body: Bytes,
}
impl Response {
// `200 OK` carrying `body`, with `content-length` set from it.
/// Returns a `200 OK` response carrying `body`, with `content-length` set
/// and no `content-type`.
pub fn ok(body: Bytes) -> Response {
let mut h: Map<String, String> = Map::new()
h.insert("content-length", "${body.len()}")
Response { status: 200, headers: h, body: body }
}
// `status` carrying `s` as UTF-8 text.
/// Returns a response with status `status` carrying `s` as UTF-8 text,
/// with `content-type` set to `text/plain; charset=utf-8` and
/// `content-length` to the text's length in bytes.
pub fn text(status: Int, s: String) -> Response {
let body = bytes_from_string(s)
let mut h: Map<String, String> = Map::new()
h.insert("content-length", "${body.len()}")
h.insert("content-type", "text/plain; charset=utf-8")
Response { status: status, headers: h, body: body }
}
/// Returns a `404 Not Found` response whose body is the text `not found`.
///
/// `Server` answers with it when no route matches.
pub fn not_found() -> Response { Response::text(404, "not found") }
// `200 OK` carrying `v` as JSON text, with `content-type` and
// `content-length` set. It takes the name section 6 gives a client-side
// decoder (`json<T: FromJson>(self)`): there is no client, and one type
// cannot have both, so the constructor has it.
/// Returns a `200 OK` response carrying `v` as JSON text, as `stringify`
/// writes it, with `content-type` set to `application/json` and
/// `content-length` set.
///
/// Panics where `stringify` does, if arrays and objects in `v` nest too
/// deep.
pub fn json(v: JsonValue) -> Response {
let body = bytes_from_string(stringify(v))
let mut h: Map<String, String> = Map::new()
h.insert("content-length", "${body.len()}")
h.insert("content-type", "application/json")
Response { status: 200, headers: h, body: body }
}
// The response as it goes on the wire.
//
// No intrinsic backs this: a response is a status line, a header block and
// a body, which Nova builds with string interpolation and `Bytes::concat`.
// That is the design's decision rather than a gap -- see
// docs/adr/0019-offset-table-intrinsic-boundary.md.
/// Returns the response as it is sent: the status line, one line per
/// header, a blank line, and the body. Each line ends with a carriage
/// return and a line feed.
///
/// The status line holds `HTTP/1.1`, the status code and a reason phrase:
/// the usual one for 200, 201, 204, 400, 404, 405, 413 and 500, and for
/// any other code a phrase for its class, such as `Client Error`. Each
/// header is written `name: value`. A header whose name or value holds a
/// carriage return or a line feed is left out, so it cannot add lines of
/// its own. Nothing else is added: no header the response does not hold.
pub fn to_bytes(self) -> Bytes {
let mut head = "HTTP/1.1 ${self.status} ${reason_phrase(self.status)}\r\n"
let names = self.headers.keys()
let mut i = 0
while i < names.len() {
let k = names[i]
match self.headers.get(k) {
// A header whose name or value carries a CR or LF is
// skipped rather than emitted: see `is_crlf_free`'s own doc
// comment for why this is the right layer to refuse it.
Some(v) => {
if is_crlf_free(k) && is_crlf_free(v) { head = "${head}${k}: ${v}\r\n" }
}
None => head = head
}
i = i + 1
}
bytes_from_string("${head}\r\n").concat(self.body)
}
}
// Whether `s` contains no CR and no LF, and so is safe to place verbatim in
// one header line.
//
// **Response splitting is a real vulnerability class, and a stdlib
// serialiser is the right layer to refuse it rather than every caller
// separately.** A header name or value carrying an embedded CRLF can
// terminate the line `to_bytes` is building early and forge arbitrary
// further header lines -- or even a second status line -- into what the
// peer reads as one response. Request-derived strings cannot carry either
// byte: `httparse` itself terminates the request line and every header
// value at CRLF, so the reachable case is a handler that writes body-derived
// or otherwise untrusted data straight into a response header.
//
// **This paragraph used to name `examples/05-json-api` as exactly that
// shape, and that example is the opposite of it.** The sentence was written
// before the example existed, so nothing checked it. `json_response` there
// sets two headers -- a literal `application/json`, and `"${b.len()}"`, an
// `Int` interpolation -- and neither can carry CR or LF, which makes it the
// pattern to copy rather than the hazard to point at. The argument for this
// check never rested on an exemplar anyway: the serialiser cannot see where
// a caller's value came from, so it refuses the bytes whatever the current
// set of callers happens to do. Whether some handler in the tree does feed
// untrusted data into a header is a search of `*.nova` for `insert(`, read
// for which hits write into a header map and where each value comes from --
// it is not this comment.
//
// `to_bytes` above skips (does not emit) any header whose name or value
// fails this check, rather than emitting it verbatim or panicking, and its
// own `Bytes`-returning signature is unchanged for that -- becoming
// fallible here was judged out of scope for this fix. A caller that must
// know whether a header was dropped needs to check its own data before
// handing it to `Response`; silently dropping is still strictly safer than
// the alternative of emitting it.
fn is_crlf_free(s: String) -> Bool { !s.contains("\r") && !s.contains("\n") }
// The reason phrase for a status code. Unknown codes get the class's own
// phrase rather than an empty string: HTTP/1.1 lets a client ignore the
// phrase entirely, so a plausible one costs nothing and reads better in a
// packet capture than a blank.
fn reason_phrase(status: Int) -> String {
if status == 200 { return "OK" }
if status == 201 { return "Created" }
if status == 204 { return "No Content" }
if status == 400 { return "Bad Request" }
if status == 404 { return "Not Found" }
if status == 405 { return "Method Not Allowed" }
if status == 413 { return "Payload Too Large" }
if status == 500 { return "Internal Server Error" }
if status < 200 { return "Informational" }
if status < 300 { return "Success" }
if status < 400 { return "Redirection" }
if status < 500 { return "Client Error" }
"Server Error"
}
// The transport half: reading a request off a connection and writing a
// response back onto one, honouring keep-alive. `std/net`'s `TcpStream` is
// the only socket type either function below touches, through the `Read` and
// `Write` impls that module defines for it -- neither function opens one.
// Only `Server::listen`, at the end of this file, binds a listener.
// Read one request from `conn`, honouring keep-alive: the caller loops, and
// this reads exactly one request's head and body, leaving anything after it
// on the connection.
//
// **This accumulates the head with `Bytes::concat`, which is O(n) per call**,
// so building an n-byte head out of k reads is O(n*k). Bounded by
// `max_head_bytes` at 8 KiB that is not worth a rope or a growable buffer;
// it is noted here so a reader knows it was weighed rather than missed.
//
// **One socket wait per task.** Staging two I/O waits in one poll ends the
// process (`stage_park` in `crates/nova-runtime/src/task.rs`), so a task
// parked in `accept` cannot also be here. A server needs one task accepting
// and one per connection -- see `std/net`'s `accept` doc comment.
//
// **Neither `conn.read` call below carries a timeout, and that is a live
// denial-of-service, not a hygiene gap.** Both park with no deadline
// (`crates/nova-runtime/src/net.rs`'s `poll_read` calls
// `stage_io_park(socket, Interest::Read, None)` -- the trailing `None` is
// the deadline argument). A peer that connects and sends nothing, or sends a
// partial head or body and then goes silent, holds this task parked
// forever, and on the single-threaded executor this project runs, one such
// connection costs one socket and one task, indefinitely, for the price of
// a single `connect`. `std/net`'s `TcpStream::read_timeout`
// (`std/net/lib.nova`) already exists and documents EOF semantics identical
// to plain `read`'s; it was not used here. That is recorded as a gap in
// this increment's own spec, not an oversight this code silently carries --
// section 6's limits table is entirely byte-valued, so no task in this
// increment was ever asked for a temporal bound. Implementing one is later
// work; disclosing its absence is not.
//
// **A pipelined second request deadlocks the connection; it is not merely
// unsupported.** This function reads into a local buffer and returns
// `body.slice(0, want)`, so any bytes already pulled off the socket past the
// end of the current request's body are discarded when it returns. A client
// that writes a second request before reading the first response -- which
// HTTP/1.1 permits -- has that second request's bytes eaten here. The server
// then waits on its next `read` for a request that has already arrived, while
// the client waits for a response that will never come: both sides block
// forever, which is a different and worse claim than "request 2 is ignored".
//
// **`tests/runtime/http_keepalive.nova` does not exercise this case.** Its
// client `await`s each reply before writing the next request, so it never
// pipelines the two requests it sends. That fixture passing is evidence this
// function's ordinary keep-alive path works; it is not evidence the pipelined
// path is safe, and a reader should not conclude otherwise from seeing it
// green.
/// Reads one request from `conn`: its head, then a body of `Content-Length`
/// bytes.
///
/// An `async fn`: it suspends the calling task while it waits for the client,
/// and other tasks run meanwhile. It waits as long as the client takes, so a
/// client that sends nothing keeps it waiting for ever; `std/time`'s
/// `timeout` can bound the wait. On a connection kept open for more requests,
/// call it again for the next one only after answering this one: any bytes
/// it read past this request's body are dropped, so a request that a client
/// sends before reading the response can be lost, and both sides then wait
/// for ever.
///
/// Returns an `HttpError` of kind `Transport` if reading fails or the
/// connection closes before the request is complete, `BodyTooLarge` if
/// `Content-Length` is more than `limits` allows, `BadHeader` if it is not a
/// whole number written in digits, and the kinds `parse_request_head`
/// returns for a malformed head. As there, the head's byte limit counts
/// every byte read so far, which can include the start of the body.
///
/// One request read from a connection and answered, over the loopback
/// address:
///
/// ```
/// async fn serve_one(listener: TcpListener) {
/// let conn = listener.accept().await.unwrap()
/// let req = read_request(conn, Limits::default()).await.unwrap()
/// let reply = Response::text(200, "got ${req.body.len()} bytes")
/// write_response(conn, reply).await.unwrap()
/// conn.close().await.unwrap()
/// }
///
/// async fn main() {
/// let listener = bind("127.0.0.1:0").unwrap()
/// let server = spawn(serve_one(listener))
/// let client = connect("127.0.0.1:${listener.local_port().unwrap()}").await.unwrap()
/// let ask = "POST /upload HTTP/1.1\r\nContent-Length: 5\r\n\r\nhello"
/// client.write(bytes_from_string(ask)).await.unwrap()
/// let reply = client.read_to_end().await.unwrap().to_string().unwrap()
/// assert(reply.starts_with("HTTP/1.1 200 OK\r\n"), reply)
/// assert(reply.ends_with("\r\n\r\ngot 5 bytes"), reply)
/// server.join().await
/// listener.close().await.unwrap()
/// }
/// ```
pub async fn read_request(conn: TcpStream, limits: Limits) -> Result<Request, HttpError> {
let mut buf = bytes_from_string("")
let mut head: Option<Request> = None
while head.is_none() {
let chunk = match conn.read(4096).await {
Ok(b) => b
Err(e) => return Err(HttpError { kind: Transport, message: e.message })
}
if chunk.len() == 0 {
return Err(HttpError { kind: Transport, message: "connection closed mid-head" })
}
buf = buf.concat(chunk)
// One arm, not two: `parse_request_head` already returns
// `Option<Request>`, so the loop assigns it straight through. It is
// also the form the language permits -- `Ok(Some(r))` is
// `E0900: nested patterns inside variants are not supported yet`.
match parse_request_head(buf, limits) {
Ok(maybe) => head = maybe
Err(e) => return Err(e)
}
}
let req = match head {
Some(r) => r
None => return Err(HttpError { kind: BadRequestLine, message: "no head after parsing" })
}
// The head's own length, so the body can be split off what was already
// read. `parse_offsets` is cheap and the buffer is unchanged, so re-reading
// `body_start` costs less than threading it out of `parse_request_head`
// and widening that function's return type for one caller.
//
// Indexed straight to the last element with no status check first --
// safe here specifically because reaching this line already required
// `parse_request_head(buf, limits)` above to return `Ok(Some(_))` on
// this same, unmutated `buf`, which happens only at status `0`; see
// `parse_offsets`'s own doc comment for the check a caller in general
// must make first.
let t = parse_offsets(buf)
let body_start = t[t.len() - 1]
let want = match content_length_of(req, limits.max_body_bytes) {
Ok(n) => n
Err(e) => return Err(e)
}
if want > limits.max_body_bytes {
return Err(HttpError { kind: BodyTooLarge, message: "Content-Length over max_body_bytes" })
}
let mut body = buf.slice(body_start, buf.len())
// **The head's "not worth engineering around" conclusion above does not
// transfer here.** `max_body_bytes` defaults to 1 MiB, not 8 KiB, and the
// same per-call-O(n) `concat` accumulation is work amplification on
// attacker-controlled input at that size, not a rounding error.
//
// **Each read below asks for exactly `want - body.len()` bytes, not a
// fixed chunk size, which changes the common case but not the worst
// one.** A peer able to deliver the whole remainder in one read now
// costs this loop exactly one concatenation, regardless of `want` --
// previously, a fixed 4096-byte ask meant even a peer that had already
// sent the entire body cost exactly 256 concatenations to drain at the 1
// MiB default (not "up to" -- with the whole body already available,
// every read but the last returned the full 4096 bytes asked for), since
// this loop could only take 4096 bytes at a time regardless of how much
// more was already there to read. `Read::read` may still legally hand
// back fewer bytes than asked for on any call, so the worst case is
// exactly what it was before: a peer that sends the
// body `k` bytes at a time forces about `want / k` reads and
// concatenations, and because each concatenation copies everything
// accumulated so far plus the new chunk, the bytes actually moved are
// triangular in the read count -- about 134,742,016 bytes copied to
// assemble a 1,048,576-byte body at 4096 bytes per read (a ~128x
// amplification), or about 537,395,200 bytes at 1024 bytes per read
// (~512x). **Both figures are floors, not ceilings**: they hold only if
// the peer hands back exactly that many bytes on every read, and since
// `Read::read` may legally return fewer, a slower or more adversarial
// peer only raises the read count and the bytes copied -- never lowers
// them. At the worse of the two, that is roughly 230 microseconds in GC
// allocations alone at this project's own measured ~900 ns each
// (`docs/superpowers/specs/2026-09-01-std-http-request-parsing-design.md`
// section 7), against the ~100 microsecond/request budget a 10k req/sec
// gate implies on a single-threaded executor. Bounded, so not unlimited,
// but a lever attacker-controlled input can still pull on the worst-case
// path, recorded here rather than eliminated: no restructuring beyond
// sizing each read to what is still wanted, and no intrinsic, are part
// of this change -- that choice is recorded for the whole-branch review.
//
// **The `body.len() > limits.max_body_bytes` check this loop used to run
// on every iteration is gone, not merely untriggered.** It could only
// ever fire on overshoot -- reading more than `want` bytes in one call,
// which needed the old fixed 4096-byte ask to happen at all -- and when
// it did fire on a legitimate request it was a false rejection: with the
// defaults, any declared length in `(1044481, 1048576]` plus one
// trailing byte pulled into the same read pushed `body.len()` past
// `max_body_bytes` while `want` itself was still under it. Reading
// exactly `want - body.len()` bytes below means `body.len()` can never
// exceed `want`, and `want` is already checked against
// `limits.max_body_bytes` above before this loop starts, so the
// allocation this loop builds is bounded exactly now, not
// approximately -- there is no overshoot left for that check to catch.
while body.len() < want {
let chunk = match conn.read(want - body.len()).await {
Ok(b) => b
Err(e) => return Err(HttpError { kind: Transport, message: e.message })
}
if chunk.len() == 0 {
return Err(HttpError { kind: Transport, message: "connection closed mid-body" })
}
body = body.concat(chunk)
}
Ok(
Request {
method: req.method,
path: req.path,
headers: req.headers,
body: body.slice(0, want),
},
)
}
// `Content-Length`, or `Ok(0)` when the header is absent.
//
// **`Content-Length` is the only body framing v1 understands.** A request
// without one is treated as bodiless; chunked transfer-encoding is out of
// scope and is not detected, so a chunked request's body is left on the
// connection and will be misread as the next request. That is a known
// limitation of v1 rather than a defect to work around here -- it is recorded
// in the design spec's section 10 and in this module's header.
//
// **Returns `Err(BadHeader)` when the header is present but is not a
// non-empty run of decimal digits, rather than defaulting it to `0`.** RFC
// 9112 section 6.3 requires rejecting such a message outright: treating
// `"5x"`, `"abc"` or `""` as `0` would accept the request with an empty body
// while the peer still has the declared bytes queued to send, which desyncs
// the connection exactly like the chunked-encoding and pipelining cases this
// module's own header already documents -- the undelivered bytes sit on the
// socket, and `read_request`'s next call parses them as the start of a new
// request. This was checked once and found NOT to be absorbed downstream:
// `Bytes::slice` clamping a negative bound to `0` (which is what the old,
// silently-defaulting version of this function relied on) only kept a
// negative `want` from corrupting memory safety -- it said nothing about the
// connection-framing consequence above, which is the one that actually
// matters here and is what this function now prevents directly.
//
// `max` is `limits.max_body_bytes`, threaded through to `digits_to_int` so a
// pathologically long digit string cannot wrap its accumulator negative; see
// that function's own doc comment.
fn content_length_of(req: Request, max: Int) -> Result<Int, HttpError> {
match req.headers.get("content-length") {
Some(v) => match digits_to_int(v.trim(), max) {
Some(n) => Ok(n)
None => Err(
HttpError {
kind: BadHeader,
message: "Content-Length is not a valid non-negative integer",
},
)
}
None => Ok(0)
}
}
// A non-empty run of decimal digits as an `Int`, or `None` if `s` is empty
// or contains anything else. Nova has no `String`-to-number conversion, so
// this walks the characters.
//
// **Capped at `max`, rather than accumulating without bound.** `Int` wraps
// on overflow (measured elsewhere in this project), so a pathologically
// long digit string could otherwise wrap the accumulator negative. The
// value this returns eventually reaches exactly one comparison against
// `max` itself: `content_length_of` passes it straight through as `want`,
// and `read_request` checks `want > limits.max_body_bytes`, the same `max`
// passed in here. So once the running total exceeds `max` the exact value
// stops mattering, it is already "too large," and returning as soon as
// that is known bounds the accumulator.
//
// **What that bound actually is, since the guarantee is conditional and an
// earlier version of this comment stated it flatly.** Every iteration that
// continues leaves `n <= max`, so the largest value `n` can reach is
// `max * 10 + 9`. That cannot wrap while `max` stays below roughly
// `Int::MAX / 10`. At the default `max_body_bytes` of 1 MiB the accumulator
// tops out around ten million, so the margin is enormous -- but the
// condition is on `max`, not on this function, and `max_body_bytes` is the
// one ceiling here with no backstop underneath it: the runtime hard-walls
// `max_head_bytes` and `max_header_count` before allocating, and never sees
// the body at all. A caller passing a `max_body_bytes` near `Int::MAX`
// would wrap this accumulator, and nothing clamps it. That value is
// absurd -- hundreds of petabytes -- which is why this is a scoping note
// rather than a check.
//
// This is narrower than a general overflow check, and is sufficient only
// because of that comparison-only usage; it is not a general-purpose
// decimal parser. One consequence of stopping early: a value
// that is both longer than `max` warrants and would eventually prove
// invalid past that point (digits followed by a stray non-digit) is
// reported as a valid, too-large number rather than a malformed one, since
// scanning stops the moment the running total is confirmed to exceed `max`
// and the remaining characters are never inspected. The caller rejects the
// request either way -- as `BodyTooLarge` instead of `BadHeader` in that one
// edge case -- so this does not change whether the request is accepted.
fn digits_to_int(s: String, max: Int) -> Option<Int> {
let cs = s.chars()
if cs.len() == 0 { return None }
let mut n = 0
let mut i = 0
while i < cs.len() {
let d = char_digit(cs[i])
if d < 0 { return None }
n = n * 10 + d
if n > max { return Some(n) }
i = i + 1
}
Some(n)
}
fn char_digit(c: Char) -> Int {
if c == '0' { return 0 }
if c == '1' { return 1 }
if c == '2' { return 2 }
if c == '3' { return 3 }
if c == '4' { return 4 }
if c == '5' { return 5 }
if c == '6' { return 6 }
if c == '7' { return 7 }
if c == '8' { return 8 }
if c == '9' { return 9 }
0 - 1
}
// Write `resp` to `conn`, looping until every byte is out.
//
// **`std/net`'s `write` is one non-blocking attempt, not a `write_all`**, and
// it returns the count it managed -- so a caller that ignored the count would
// silently truncate a response under back-pressure. Returns the total written,
// which equals the response's length on success.
/// Writes all of `resp` to `conn`, laid out as `to_bytes` lays it out, and
/// returns the number of bytes written.
///
/// An `async fn`: it suspends the calling task while the connection has no
/// room for more, and other tasks run meanwhile. Returns the `IoError` a
/// write fails with, or one of kind `Other` if a write makes no progress.
pub async fn write_response(conn: TcpStream, resp: Response) -> Result<Int, IoError> {
let wire = resp.to_bytes()
let total = wire.len()
let mut sent = 0
while sent < total {
let n = match conn.write(wire.slice(sent, total)).await {
Ok(n) => n
Err(e) => return Err(e)
}
if n <= 0 { return Err(IoError { kind: Other, message: "write made no progress" }) }
sent = sent + n
}
Ok(total)
}
// ---------------------------------------------------------------------------
// The router: `Server`.
//
// Written in Nova over the pieces above. Handlers are `fn(Request) ->
// Response` values -- this module's header says why not section 6's
// `Handler` alias -- so a handler is synchronous: it builds its response
// without awaiting.
// How often `Server::listen` and every connection it serves look at the
// shutdown flag, in milliseconds. A signal is noticed within one tick.
const SERVER_TICK_MS: Int = 100
// How long one request may take, in milliseconds: from the moment `serve`
// takes it up until its response has been written. A stalled client is
// dropped at this deadline, so a shutdown always finishes.
const SERVER_REQUEST_MS: Int = 10000
// A routing table and nothing else. Build one with `Server::new()` and `get`,
// then `listen`.
/// A router that serves GET routes over HTTP/1.1.
///
/// Build one with `Server::new()` and a call to `get` for each route, then
/// serve it with `listen`. A request goes to the handler of the route whose
/// path equals the request's target exactly, query string included; any
/// other target, and any method but `GET`, gets `Response::not_found()`.
pub record Server { routes: Vec<Route> }
// One GET route: an exact path, and the handler that answers it.
record Route { path: String, handler: fn(Request) -> Response }
// A request read off a connection, and the bytes that followed it: a
// pipelined next request, or the start of one.
record InFlight { req: Request, rest: Bytes }
impl Server {
/// Returns a server with no routes.
pub fn new() -> Server { Server { routes: Vec::new() } }
// Adds a GET route. Takes `self` rather than `mut self`, and returns
// `Server` rather than section 6's `Self`: a `mut self` call on the
// temporary `Server::new()` returns is `E0060`, and `Self` as a type in an
// impl block is `E0001`.
//
// The new server gets a copy of the route table, and `self` is left
// alone. A `Vec` is shared, not copied, on assignment, so pushing onto
// `self.routes` would add the route to every server built from the same
// base as well.
/// Returns a copy of this server with one more route: `GET` requests for
/// `path` go to `handler`.
///
/// `self` is left unchanged, so calls chain, as in
/// `Server::new().get("/", home).get("/about", about)`. `path` must equal
/// a request's target exactly: `/a` matches neither `/a/` nor `/a?x=1`. Of
/// two routes with the same path, the one added first answers.
pub fn get(self, path: String, handler: fn(Request) -> Response) -> Server {
let mut rs: Vec<Route> = Vec::new()
let mut i = 0
while i < self.routes.len() {
match self.routes.get(i) {
Some(r) => rs.push(r)
None => {}
}
i = i + 1
}
rs.push(Route { path: path, handler: handler })
Server { routes: rs }
}
// The response to `req`: the handler of the GET route whose path equals
// `req.path` exactly, or `Response::not_found()` for any other path or
// method. Socket-free, so a test can call it directly.
/// Returns the response to `req`: what the handler of the route whose
/// path equals `req.path` returns, or `Response::not_found()` if there is
/// none.
///
/// Only a `GET` request reaches a handler; any other method, `HEAD`
/// included, gets `not_found`. It touches no connection, so a test can
/// call it directly with a `Request` it builds.
pub fn dispatch(self, req: Request) -> Response {
match req.method {
Get => {
let mut i = 0
while i < self.routes.len() {
match self.routes.get(i) {
Some(r) => {
if r.path == req.path {
// Bound to a local first: calling a fn-typed
// field directly, `r.handler(req)`, is `E0014`.
let h = r.handler
return h(req)
}
}
None => {}
}
i = i + 1
}
Response::not_found()
}
_ => Response::not_found()
}
}
// Serves `addr` until the process is asked to stop: SIGTERM or SIGINT on
// Unix, Ctrl+Break or Ctrl+C on Windows.
//
// **Graceful.** On the first such signal it stops accepting within one
// tick, closes the listener, and returns `Ok(())`. Connections are not
// touched: each finishes the request it has started, and any pipelined
// request that has already arrived whole, within that request's deadline,
// then closes; an idle one closes at its next tick. A second signal ends
// the process at once, by the signal's default action.
//
// **It returns before those connections finish.** Their tasks run on
// alongside any code after `.await`, and the process exits only once
// `block_on`'s implicit join has finished them
// (docs/adr/0009-async-execution-model.md).
//
// **The signal handler is installed by the first `listen`, before `bind`,
// and stays installed for the rest of the process.** So a client that can
// connect can also stop the server gracefully. But a program that recovers
// from this function's `Err` and keeps running swallows its next signal,
// and only a second one ends it; docs/adr/0022-process-shutdown-signals.md
// records why.
//
// **Errors.** A failed `bind` or `accept` returns `Err`. An accept error
// closes the listener first and leaves the connections running; the flag
// is not set, so they keep being served.
/// Serves HTTP on `addr`, a `"host:port"` string such as
/// `"127.0.0.1:8080"`, until the process is asked to stop.
///
/// An `async fn`: it suspends the calling task while it waits for
/// connections, and serves each connection in a task of its own. A
/// handler that panics ends the whole program.
///
/// A connection stays open for more requests until the client closes it,
/// and they are answered in order, including requests sent before the
/// previous response arrived; a `Connection: close` header does not close
/// it. Requests are limited as `Limits::default()` says. A malformed or
/// oversized request, or one not read and answered within 10 seconds of
/// the server starting on it, closes its connection without a response.
/// No `100 Continue` is ever sent.
///
/// On SIGTERM or SIGINT (Ctrl+C or Ctrl+Break on Windows) it stops
/// accepting within about a tenth of a second, and returns `Ok(())`.
/// Connections already open are not cut off: each finishes the request it
/// has started, and any further request that has already arrived whole,
/// then closes; an idle one closes within about a tenth of a second. The
/// program does not end until they have, and a second signal ends it at
/// once. Once `listen` has run, the program goes on catching those
/// signals, even after `listen` returns an `Err`: a program that carries
/// on after that is not ended by the next signal, only by a second.
///
/// Returns an `IoError` if `addr` cannot be bound, as when it is already
/// in use, or if accepting a connection fails. After a failed accept the
/// connections already open go on being served.
pub async fn listen(self, addr: String) -> Result<(), IoError> {
let _ = shutdown_requested()
let l = match bind(addr) {
Ok(l) => l
Err(e) => return Err(e)
}
// The flag is checked on every turn, not only when an accept times
// out: under steady traffic the timeout arm may never run. The accept
// is polled under `timeout` because a bare one never returns without a
// connection, and closing the listener under it does not wake it.
while !shutdown_requested() {
match timeout(Duration::from_millis(SERVER_TICK_MS), l.accept()).await {
Ok(r) => {
match r {
Ok(conn) => { let _ = spawn(serve(self, conn)) }
Err(e) => {
let _ = l.close().await
return Err(e)
}
}
}
Err(_) => {}
}
}
let _ = l.close().await
Ok(())
}
}
// One connection, for its whole life, with one buffer it keeps throughout.
//
// - **Idle (buffer empty):** waits for a request's first bytes one tick at a
// time. EOF closes; a timeout closes once the shutdown flag is set and
// otherwise keeps waiting.
// - **In flight (buffer not empty):** a request is taken up -- its deadline
// starts now -- read to completion by `read_in_flight`, and answered. The
// response is written under the same deadline, so a peer that stops
// reading cannot hold the connection forever either. A request taken up
// after a stop gets no further reads: it is answered only if it has already
// arrived whole, so a peer that keeps sending cannot hold the connection
// open either.
// - **After an answer:** bytes left in the buffer are the next request,
// answered even after a stop if they hold all of it. With the buffer
// empty, the connection closes if the flag is set and goes idle otherwise.
//
// Anything else that goes wrong -- EOF mid-request, a read or write error, a
// malformed head, an oversized body, a deadline passing -- closes this
// connection without a response, and touches no other.
async fn serve(s: Server, conn: TcpStream) {
let mut buf = bytes_from_string("")
let mut open = true
while open {
if buf.len() == 0 {
match conn.read_timeout(4096, SERVER_TICK_MS).await {
Ok(chunk) => { if chunk.len() == 0 { open = false } else { buf = chunk } }
Err(e) => {
match e.kind {
TimedOut => { if shutdown_requested() { open = false } }
_ => open = false
}
}
}
} else {
let started = Instant::now()
let may_read = !shutdown_requested()
match read_in_flight(conn, buf, started, may_read).await {
Some(f) => {
buf = f.rest
let left = SERVER_REQUEST_MS - started.elapsed().as_millis()
match timeout(
Duration::from_millis(left),
write_response(conn, s.dispatch(f.req)),
).await {
Ok(w) => {
match w {
Ok(n) => {
if buf.len() == 0 && shutdown_requested() { open = false }
}
Err(e) => open = false
}
}
Err(_) => open = false
}
}
None => open = false
}
}
}
let _ = conn.close().await
}
// The request at the front of `first`, reading more from `conn` as needed, or
// `None` to close the connection. The head is parsed from at most
// `max_head_bytes` of the buffer, so bytes buffered behind it -- a body, or a
// pipelined request -- never count against the head's own limit. With
// `may_read` false, for a request taken up after a stop, nothing more is
// read, and a request not already whole in `first` is `None`.
async fn read_in_flight(
conn: TcpStream,
first: Bytes,
started: Instant,
may_read: Bool,
) -> Option<InFlight> {
let limits = Limits::default()
let mut buf = first
let mut head: Option<Request> = None
while head.is_none() {
let window = head_window(buf, limits.max_head_bytes)
match parse_request_head(window, limits) {
Ok(maybe) => head = maybe
Err(_) => return None
}
if head.is_none() {
// A head still incomplete at `max_head_bytes` never will be.
if window.len() >= limits.max_head_bytes { return None }
if !may_read { return None }
match read_more(conn, 4096, started).await {
Some(chunk) => buf = buf.concat(chunk)
None => return None
}
}
}
let req = match head {
Some(r) => r
None => return None
}
// `parse_request_head` returned `Ok(Some(_))` on this same window, so its
// offset table has status 0 and its last element is where the body starts.
let t = parse_offsets(head_window(buf, limits.max_head_bytes))
let body_start = t[t.len() - 1]
let want = match content_length_of(req, limits.max_body_bytes) {
Ok(n) => n
Err(_) => return None
}
if want > limits.max_body_bytes { return None }
let end = body_start + want
while buf.len() < end {
if !may_read { return None }
match read_more(conn, end - buf.len(), started).await {
Some(chunk) => buf = buf.concat(chunk)
None => return None
}
}
Some(
InFlight {
req: Request {
method: req.method,
path: req.path,
headers: req.headers,
body: buf.slice(body_start, end),
},
rest: buf.slice(end, buf.len()),
},
)
}
// Up to `max` more bytes for the request taken up at `started`, or `None` at
// EOF, on a read error, or once that request's deadline has passed.
async fn read_more(conn: TcpStream, max: Int, started: Instant) -> Option<Bytes> {
let left = SERVER_REQUEST_MS - started.elapsed().as_millis()
if left <= 0 { return None }
match conn.read_timeout(max, left).await {
Ok(chunk) => {
if chunk.len() == 0 { return None }
Some(chunk)
}
Err(_) => None
}
}
// The first `max` bytes of `buf`, or all of it if it is shorter.
fn head_window(buf: Bytes, max: Int) -> Bytes {
if buf.len() <= max { return buf }
buf.slice(0, max)
}