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
Not documented.
case Head
case Head
Not documented.
case Post
case Post
Not documented.
case Put
case Put
Not documented.
case Delete
case Delete
Not documented.
case Patch
case Patch
Not documented.
case Options
case Options
Not documented.
case Trace
case Trace
Not documented.
case Connect
case Connect
Not documented.
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<Method, 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
Not documented.
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 ok
static ok = Status 200
Not documented.
const created
static created = Status 201
Not documented.
const accepted
static accepted = Status 202
Not documented.
const noContent
static noContent = Status 204
Not documented.
const movedPermanently
static movedPermanently = Status 301
Not documented.
const found
static found = Status 302
Not documented.
const seeOther
static seeOther = Status 303
Not documented.
const notModified
static notModified = Status 304
Not documented.
const temporaryRedirect
static temporaryRedirect = Status 307
Not documented.
const permanentRedirect
static permanentRedirect = Status 308
Not documented.
const badRequest
static badRequest = Status 400
Not documented.
const unauthorized
static unauthorized = Status 401
Not documented.
const forbidden
static forbidden = Status 403
Not documented.
const notFound
static notFound = Status 404
Not documented.
const methodNotAllowed
static methodNotAllowed = Status 405
Not documented.
const notAcceptable
static notAcceptable = Status 406
Not documented.
const requestTimeout
static requestTimeout = Status 408
Not documented.
const conflict
static conflict = Status 409
Not documented.
const gone
static gone = Status 410
Not documented.
const lengthRequired
static lengthRequired = Status 411
Not documented.
const contentTooLarge
static contentTooLarge = Status 413
Not documented.
const uriTooLong
static uriTooLong = Status 414
Not documented.
const unsupportedMediaType
static unsupportedMediaType = Status 415
Not documented.
const unprocessableContent
static unprocessableContent = Status 422
Not documented.
const tooManyRequests
static tooManyRequests = Status 429
Not documented.
const requestHeaderFieldsTooLarge
static requestHeaderFieldsTooLarge = Status 431
Not documented.
const internalServerError
static internalServerError = Status 500
Not documented.
const notImplemented
static notImplemented = Status 501
Not documented.
const badGateway
static badGateway = Status 502
Not documented.
const gatewayTimeout
static gatewayTimeout = Status 504
Not documented.
const httpVersionNotSupported
static httpVersionNotSupported = Status 505
Not documented.
fn of
static fn of(code: Int): Result<Status, 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: Status): Ordering
Not documented.
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>): Headers
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 = Body.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 = Body.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 = Body.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): Body
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(): Body
A body with nothing in it, e.g. for a GET request.
fn jsonOf
static fn jsonOf<Value: Encode>(value: Value): Body
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): Body
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): Body
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
Not documented.
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()
Not documented.
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.
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
Not documented.
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.
fn of
static fn of(status: Status): Response
A response of status with no body: Response.of(Status.notFound).
fn text
static fn text(content: String, status: Status = Status.ok): Response
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): Response
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.