std/http/message

std/http/src/message.trb

The parts of an HTTP message: Method, Status, Headers, the Body that is a stream, and the two messages, Request and Response (docs/design/NETWORK.md section 6).

type Method

type Method with Show, Equals, Hash

An HTTP method: a case per method RFC 9110 registers, Query (RFC 10008) and WebDAV's Search (RFC 5323), and Other for the rest - the set is open (a WebDAV server meets PROPFIND), so a router matches on the cases and still sees every method.

match request.method {
  .Get => ...
  .Post => ...
  _ => ...
}

Method.of is the one way in from text: it answers the case for a registered name and Other only for the rest, so GET is never Other("GET"). A name is case-sensitive, as RFC 9110 section 9.1 says: get is Other("get").

case Get

case Get

GET (RFC 9110 section 9.3.1): the representation of the target. Safe and idempotent.

case Head

case Head

HEAD (RFC 9110 section 9.3.2): a GET whose response carries the head and no content.

case Post

case Post

POST (RFC 9110 section 9.3.3): the request's content is processed by the target. Neither safe nor idempotent.

case Put

case Put

PUT (RFC 9110 section 9.3.4): the target's state is replaced by the request's content. Idempotent.

case Delete

case Delete

DELETE (RFC 9110 section 9.3.5): the target is removed. Idempotent.

case Patch

case Patch

PATCH (RFC 5789): a partial change of the target, described by the request's content.

case Options

case Options

OPTIONS (RFC 9110 section 9.3.7): what the target or the server supports. Safe and idempotent.

case Trace

case Trace

TRACE (RFC 9110 section 9.3.8): the request is echoed back, to see what the path changed. Safe.

case Connect

case Connect

CONNECT (RFC 9110 section 9.3.6): a tunnel to the host and port of the target.

case Query

case Query

QUERY (RFC 10008, June 2026): a query in the request's content rather than in the URI - safe and idempotent like GET, with a body like POST, and its response cacheable with the content as part of the key.

case Search

case Search

SEARCH of WebDAV (RFC 5323): a search whose request is the body, safe and idempotent.

case Other

case Other(name: String)

A method none of the cases names. Made by Method.of, which never makes one for a name a case has.

fn of

static fn of(name: String): Result<Self, HttpError>

The method of that name. A name that is not a token of RFC 9110 - empty, with a space, a control character or a separator - is no method.

fn name

fn name(): String

"GET".

fn isSafe

fn isSafe(): Bool

GET, HEAD, OPTIONS, TRACE, QUERY and SEARCH: a request of these asks and changes nothing (RFC 9110 section 9.2.1, RFC 10008 section 2, RFC 5323 section 2). Other is not: nothing is known of it.

fn isIdempotent

fn isIdempotent(): Bool

The safe methods, PUT and DELETE: sending a request of these twice has the effect of sending it once (RFC 9110 section 9.2.2), so a client may send it again where the connection failed before an answer arrived.

fn carriesContent

fn carriesContent(): Bool

POST, PUT, PATCH, QUERY and SEARCH: a request of these carries content, and announces even an empty one.

fn show

fn show(): String

The name, as Method.name answers it: GET, and for an Other the name it holds.

type Status

type Status with Show, Equals, Hash, Compare

An HTTP status: a capsule over its code, from 100 to 599, with the common ones as constants.

Response.text("no such user", status: Status.notFound)

const switchingProtocols

static switchingProtocols = Self 101

101 Switching Protocols: the server takes the connection over for another protocol; see Response.upgrade.

const ok

static ok = Self 200

200 OK: the request succeeded and the response carries the result.

const created

static created = Self 201

201 Created: the request made a new resource.

const accepted

static accepted = Self 202

202 Accepted: the request is taken, and its processing is not done.

const noContent

static noContent = Self 204

204 No Content: the request succeeded and there is nothing to send back.

const movedPermanently

static movedPermanently = Self 301

301 Moved Permanently: the resource lives at the Location from now on.

const found

static found = Self 302

302 Found: the resource is at the Location for now.

const seeOther

static seeOther = Self 303

303 See Other: the answer is at the Location, to be fetched with a GET.

const notModified

static notModified = Self 304

304 Not Modified: the copy the client holds is current.

const temporaryRedirect

static temporaryRedirect = Self 307

307 Temporary Redirect: the resource is at the Location for now, and the method and the body stay.

const permanentRedirect

static permanentRedirect = Self 308

308 Permanent Redirect: the resource lives at the Location from now on, and the method and the body stay.

const badRequest

static badRequest = Self 400

400 Bad Request: the request is malformed or invalid.

const unauthorized

static unauthorized = Self 401

401 Unauthorized: the request needs credentials, or the ones it carries are not accepted.

const forbidden

static forbidden = Self 403

403 Forbidden: the server understood the request and refuses it.

const notFound

static notFound = Self 404

404 Not Found: there is no resource at the target.

const methodNotAllowed

static methodNotAllowed = Self 405

405 Method Not Allowed: the target does not support the method; Allow lists the ones it does.

const notAcceptable

static notAcceptable = Self 406

406 Not Acceptable: there is no representation the request's Accept fields allow.

const requestTimeout

static requestTimeout = Self 408

408 Request Timeout: the server gave up waiting for the rest of the request.

const conflict

static conflict = Self 409

409 Conflict: the request clashes with the current state of the resource.

const gone

static gone = Self 410

410 Gone: the resource is no longer here and will not come back.

const lengthRequired

static lengthRequired = Self 411

411 Length Required: the request needs a Content-Length.

const contentTooLarge

static contentTooLarge = Self 413

413 Content Too Large: the request's content is larger than the server accepts.

const uriTooLong

static uriTooLong = Self 414

414 URI Too Long: the target is longer than the server accepts.

const unsupportedMediaType

static unsupportedMediaType = Self 415

415 Unsupported Media Type: the server does not take the request's content type.

const unprocessableContent

static unprocessableContent = Self 422

422 Unprocessable Content: the content is well formed and cannot be processed.

const tooManyRequests

static tooManyRequests = Self 429

429 Too Many Requests: the client sent more than the server allows in a time; Retry-After may say when to return.

const requestHeaderFieldsTooLarge

static requestHeaderFieldsTooLarge = Self 431

431 Request Header Fields Too Large: the fields of the request's head are too large, together or one.

const internalServerError

static internalServerError = Self 500

500 Internal Server Error: the server failed in a way no other code names.

const notImplemented

static notImplemented = Self 501

501 Not Implemented: the server does not support what the request needs.

const badGateway

static badGateway = Self 502

502 Bad Gateway: a server upstream answered with something invalid.

const serviceUnavailable

static serviceUnavailable = Self 503

503 Service Unavailable: the server cannot answer now; Retry-After may say when to return.

const gatewayTimeout

static gatewayTimeout = Self 504

504 Gateway Timeout: a server upstream did not answer in time.

const httpVersionNotSupported

static httpVersionNotSupported = Self 505

505 HTTP Version Not Supported: the server does not speak the request's HTTP version.

fn of

static fn of(code: Int): Result<Self, HttpError>

The status of that code: 100 to 599, which is every code HTTP can carry.

fn code

fn code(): Int

404.

fn reason

fn reason(): String

The phrase RFC 9110 registers for the code, or "" for a code it does not.

fn isInformational

fn isInformational(): Bool

1xx.

fn isSuccess

fn isSuccess(): Bool

2xx.

fn isRedirect

fn isRedirect(): Bool

3xx.

fn isClientError

fn isClientError(): Bool

4xx.

fn isServerError

fn isServerError(): Bool

5xx.

fn show

fn show(): String

404 Not Found, or the code alone where no phrase is registered.

fn compare

fn compare(other: Self): Ordering

By the code: Status.notFound is greater than Status.ok.

type Headers

type Headers with Show, Equals

The fields of a head, in the order they arrived. A name is compared without regard to case and kept as it was written; a name may repeat (Set-Cookie), which is why this is not a Map.

var headers = Headers()
headers.set "Content-Type", "text/plain"
print headers.get("content-type")

A value that holds a line break or a NUL is written with spaces in their place, and a field whose name is not a token is not written at all: a header built from what a client sent cannot smuggle a second header in.

fn get

fn get(name: String): String?

The first value of name, or None.

fn all

fn all(name: String): List<String>

Every value of name, in order.

fn contains

fn contains(name: String): Bool

Whether a field of that name is there.

fn length

fn length(): Int

How many fields there are, repeated names counted each time.

fn set

var fn set(name: String, value: String)

Replaces every field of name by one with value, where the first of them was; at the end where there was none.

fn add

var fn add(name: String, value: String)

Adds a field, also where one of that name is there already.

fn remove

var fn remove(name: String)

Removes every field of name.

fn entries

fn entries(): List<(name: String, value: String)>

Every field, in order: for (name, value) in headers.entries() { ... }.

fn show

fn show(): String

Content-Type: text/plain, one field per line.

extend Headers with From<Map<String, String>>

extend Headers with From<Map<String, String>>

Headers.from(["Accept": "application/json"]): the entries of a map, in its order.

fn from

static fn from(value: Map<String, String>): Self

type Body

shared type Body with Source<Bytes, HttpError>

The bytes of a request or a response, as a stream that is read once.

const all = response.body.through(Json.items<User>()).checked().toList().await()?   // one at a time
const text = response.body.text().await()?                                          // or all of it
File.write(path, response.body.mapFailure(IoError.from)).await()?  // or straight to disk

The convenience methods come first on purpose - that is the lesson of fetch and Bun, where await response.json() is what almost every program wants - and every one of them takes a limit, because a peer that decides how much memory a program allocates is a denial of service.

const defaultLimit

static defaultLimit: Int = 16777216

16 MiB: large enough for any document a program means to hold in one piece, small enough that a body nobody expected is refused instead of filling the machine. Streaming is the way past it, not a bigger number.

fn next

var fn next(): Task<Result<Bytes?, HttpError>>

The next chunk, or None at the end of the body. Most callers reach for Body.bytes or Body.text instead.

fn close

var fn close()

Nothing of its own: reading is a field, and the release of the body releases it - and with it the underlying connection - right after this, without reading the rest.

fn length

fn length(): Int?

How many bytes the body has, where that is known before it is read.

fn bytes

var fn bytes(limit: Int = Self.defaultLimit): Task<Result<Bytes, HttpError>>

The whole body. More than limit bytes is an HttpError and the rest is not read.

fn text

var fn text(limit: Int = Self.defaultLimit): Task<Result<String, HttpError>>

The whole body as text. Bytes that are not UTF-8 are an HttpError, never a replacement character.

fn json

var fn json<Value: Decode>(limit: Int = Self.defaultLimit): Task<Result<Value, HttpError>>

The whole body, decoded as JSON: response.body.json<User>().await()?

fn lines

var fn lines(): Source<String, HttpError>

Lines of text, as a stream: a log, an event stream, a CSV of any size.

fn of

static fn of(var source: Source<Bytes, HttpError>, length: Int? = None): Self

A body of any size - what an upload or a proxy hands on - with its length where the source knows it. Not Body.from(source): From.from takes its argument by value, and handing a stream over needs the permission to read it (var), because the Body reads it from now on.

fn empty

static fn empty(): Self

A body with nothing in it, e.g. for a GET request.

fn jsonOf

static fn jsonOf<Value: Encode>(value: Value): Self

An Encode value as a JSON body. Not Body.json(value): a type has one namespace for its members, and json is taken by the reading side, where it matters more (body.json<User>()).

extend Body with From<String>

extend Body with From<String>

Body.from("hello") - the text as UTF-8.

fn from

static fn from(value: String): Self

extend Body with From<Bytes>

extend Body with From<Bytes>

Body.from(bytes) - one chunk, already in memory.

fn from

static fn from(value: Bytes): Self

type Request

shared type Request

A request: what a client sends and what a server's handler is handed. A shared type and not a value, because it owns its body, a stream that is read once.

fn handle(request: Request): Task<Result<Response, HttpError>> {
  if request.path() == "/hello" {
    return Ok Response.text("hello")
  }
  Ok Response.of(Status.notFound)
}

field method

method: Method

What the request asks for: Method.Get, Method.Post, ...

field target

target: String

The request target as it was sent: /users/7?details=true, or *, or host:443 for CONNECT.

field uri

uri: Uri

The target URI RFC 9112 section 3.3 rebuilds from the target, the Host field and whether the connection is TLS: https://example.test/users/7?details=true. Normalized, so a router sees a path whose dot segments are gone.

field headers

headers: Headers = Headers()

The fields of the request's head, in the order they arrived.

field body

var body: Body

var, because reading a body changes it: whoever reads one needs a var path to it.

field remote

remote: SocketAddress? = None

Where the request came from, on a server; None on a client.

field version

version: String = "HTTP/1.1"

HTTP/1.1 or HTTP/1.0.

fn path

fn path(): String

The path of the target URI: /users/7.

fn segments

fn segments(): List<String>

The path of the target URI split at its /, still percent-encoded: ["users", "7"].

fn query

fn query(): String?

The query of the target URI, or None.

alias Upgrade

type Upgrade = (duplex: Duplex) => Task<Result<Void, HttpError>>

What a handler does with a connection it switched to another protocol (see Response.upgrade): it is handed the Duplex once the 101 is written, and the connection is its own until it finishes. The failure type is the handler's: a failing or cancelled upgrade closes only its connection, and nothing of the failure is sent.

type Response

shared type Response

What came back from a server, or what a handler answers. A shared type and not a value: it owns one end of a stream that is read once, and a copy would promise a second read of a body that is already gone. So a response that is read from sits in a var binding, for the same reason a File does.

field status

status: Status

The outcome of the request: Status.ok, Status.notFound, ...

field headers

var headers: Headers = Headers()

var, so that a handler can add a field to a response it made.

field body

var body: Body

var, because reading a body changes it: whoever reads one needs a var path to it.

field takeover

var takeover: Upgrade? = None

What takes the connection over after a 101 is written, on a server: None for every ordinary response, and Some for the one Response.upgrade makes. A client never sees one.

fn of

static fn of(status: Status): Self

A response of status with no body: Response.of(Status.notFound).

fn upgrade

static fn upgrade(handler: Upgrade, headers: Headers = Headers()): Self

101 Switching Protocols with the given fields and no body; once the server has written it, handler is handed the connection as a Duplex and the connection is no longer HTTP's. The protocol's own fields (Upgrade, Connection: Upgrade, and whatever the protocol adds) are the handler's to set in headers: the server writes them as they are and adds no framing of its own, so the mechanism carries any protocol.

Examples

fn echo(duplex: Duplex): Task<Result<Void, HttpError>> {
  var incoming = duplex.source()
  var outgoing = duplex.sink()
  while const Some(chunk) = incoming.next().await()? {
    outgoing.add(chunk).await()?
  }
  outgoing.end().await()
}

fn handle(request: Request): Task<Result<Response, HttpError>> {
  var headers = Headers()
  headers.set "Upgrade", "echo"
  headers.set "Connection", "Upgrade"
  Ok Response.upgrade(echo, headers)
}

fn text

static fn text(content: String, status: Status = Status.ok): Self

A text as UTF-8, with Content-Type: text/plain; charset=utf-8.

fn jsonOf

static fn jsonOf<Value: Encode>(value: Value, status: Status = Status.ok): Self

An Encode value as JSON, with Content-Type: application/json.

fn json

var fn json<Value: Decode>(limit: Int = Body.defaultLimit): Task<Result<Value, HttpError>>

Decodes the body as JSON. A Task, because the body arrives over the network and decoding it means reading it.