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 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.