std/http
HTTP client and server, backed by Go’s net/http.
The module is split the way every co-located standard adapter is: the data
is Nomi and only the effects are Go. Method, Status, Header, Query,
Cookie, Request, Response and Error are ordinary Nomi values you
could have written yourself; Client and Server are opaque handles
because each one names a live OS resource. Everything a program observes
about a request or a response is an immutable Nomi value.
Three things are worth knowing before reading further:
- Headers are case-INSENSITIVE and canonicalised by
Header.canonical; query parameters are case-SENSITIVE and are a separate type,Query. Sharing one type between them corrupts?userId=7&Tag=a&tag=b. - A handler runs concurrently with every other handler. There is no process-wide lock.
- A handler’s
Request.contextcarries the request’s cancellation, soRequest.canceled?reports a client disconnect or a shutdown, and aClientcall made with that context is cancelled along with it.
Import with import std/http.
Exports
Section titled “Exports”struct Method
Section titled “struct Method”struct Method { name: String }
An HTTP request method, held as the wire token it is sent as.
A newtype over the token rather than a closed enum, because HTTP’s method
space is OPEN (RFC 9110 §9). That is a modelling decision, not a
workaround. An enum carrying both Get and Custom String has two
spellings for one method, and two spellings for one value is a
representation redundancy: every equality, hash and match then has to
decide which spelling it meant, and the answers drift. Here GET has
exactly one representation, so Method.from_string("GET") and
Method.get() are the same value by construction.
Rust’s http crate reaches the same shape for the same reason
(struct Method with associated constants); Go uses bare strings.
Matching is on the token, not on a variant:
case Method.string(request.method) { "GET" -> read(request) "POST" -> write(request) _ -> Response.text(Status.method_not_allowed(), "") }
Methods are case-sensitive: Method.new("get") is not Method.get().
Interactive Tests
assert Method.new("GET") == Method.get()
refute Method.new("get") == Method.get()
assert Hashable.hash(Method.new("GET")) == Hashable.hash(Method.get())
Method.new
Section titled “Method.new”fn new(name: String): Method
type function on Method
A method from its wire token, verbatim.
Interactive Tests
assert Method.string(Method.new("QUERY")) == "QUERY"
assert Method.new("GET") == Method.get()
refute Method.new("Get") == Method.get()
Method.from_string
Section titled “Method.from_string”fn from_string(name: String): Method
type function on Method
A method from its wire token, verbatim. Alias of Method.new for
symmetry with Method.string.
Interactive Tests
assert Method.from_string("PATCH") == Method.patch()
assert Method.from_string("PATCH") == Method.new("PATCH")
Method.string
Section titled “Method.string”fn string(method: Method): String
type function on Method
The wire token for a method.
Interactive Tests
assert Method.string(Method.post()) == "POST"
assert Method.string(Method.new("BREW")) == "BREW"
Method.get
Section titled “Method.get”fn get(): Method
type function on Method
GET.
Interactive Tests
assert Method.string(Method.get()) == "GET"
assert Method.get() == Method.new("GET")
Method.head
Section titled “Method.head”fn head(): Method
type function on Method
HEAD.
Interactive Tests
assert Method.string(Method.head()) == "HEAD"
assert Method.head() == Method.new("HEAD")
Method.post
Section titled “Method.post”fn post(): Method
type function on Method
POST.
Interactive Tests
assert Method.string(Method.post()) == "POST"
assert Method.post() == Method.new("POST")
Method.put
Section titled “Method.put”fn put(): Method
type function on Method
PUT.
Interactive Tests
assert Method.string(Method.put()) == "PUT"
assert Method.put() == Method.new("PUT")
Method.patch
Section titled “Method.patch”fn patch(): Method
type function on Method
PATCH.
Interactive Tests
assert Method.string(Method.patch()) == "PATCH"
assert Method.patch() == Method.new("PATCH")
Method.delete
Section titled “Method.delete”fn delete(): Method
type function on Method
DELETE.
Interactive Tests
assert Method.string(Method.delete()) == "DELETE"
assert Method.delete() == Method.new("DELETE")
Method.connect
Section titled “Method.connect”fn connect(): Method
type function on Method
CONNECT.
Interactive Tests
assert Method.string(Method.connect()) == "CONNECT"
assert Method.connect() == Method.new("CONNECT")
Method.options
Section titled “Method.options”fn options(): Method
type function on Method
OPTIONS.
Interactive Tests
assert Method.string(Method.options()) == "OPTIONS"
assert Method.options() == Method.new("OPTIONS")
Method.trace
Section titled “Method.trace”fn trace(): Method
type function on Method
TRACE.
Interactive Tests
assert Method.string(Method.trace()) == "TRACE"
assert Method.trace() == Method.new("TRACE")
Method.equal?
Section titled “Method.equal?”fn equal?(a: Method, b: Method): Bool
impl Equatable.equal?
Two methods are equal when their wire tokens are. Hand-written rather
than derived so Equatable.equal? and == cannot drift apart: == on
a struct dispatches here, and this is the same comparison it would make
structurally.
Interactive Tests
assert Equatable.equal?(Method.new("GET"), Method.get())
refute Equatable.equal?(Method.new("get"), Method.get())
Method.to_string
Section titled “Method.to_string”fn to_string(method: Method): String
impl Display.to_string
The wire token.
Interactive Tests
assert Display.to_string(Method.get()) == "GET"
Method.inspect
Section titled “Method.inspect”fn inspect(method: Method): String
impl Debug.inspect
Interactive Tests
assert Debug.inspect(Method.get()) == "Method(GET)"
Method.hash
Section titled “Method.hash”fn hash(value: Method): Int
impl Hashable.hash
struct Status
Section titled “struct Status”struct Status { code: Int }
An HTTP response status, held as the numeric code it is sent as.
A newtype over the code for the same modelling reason as Method: an HTTP
status IS a three-digit integer, the registry is extensible, and the named
statuses are conveniences over the number rather than a second kind of
value. So Status.from_code(200) == Status.ok() holds by construction,
and there is no second spelling for anyone to compare unequally.
Rust’s http crate is struct StatusCode(NonZeroU16) with associated
constants for the same reason; Go uses bare ints.
Matching is on the code, not on a variant:
case response.status.code { 200 -> parse(response) 404 -> Maybe.None _ -> retry(response) }
A single status compares directly: if response.status == Status.ok().
Interactive Tests
assert Status.from_code(200) == Status.ok()
assert Hashable.hash(Status.from_code(200)) == Hashable.hash(Status.ok())
assert Status.from_code(299).code == 299
Status.from_code
Section titled “Status.from_code”fn from_code(code: Int): Status
type function on Status
A status from its numeric code. Any code is representable, including extension codes no constructor below names.
Interactive Tests
assert Status.from_code(404) == Status.not_found()
assert Status.from_code(599).code == 599
assert Status.text(Status.from_code(599)) == ""
Status.text
Section titled “Status.text”fn text(status: Status): String
type function on Status
The reason phrase for a status, or "" for a code with no registered
phrase. Matches Go’s http.StatusText.
Interactive Tests
assert Status.text(Status.ok()) == "OK"
assert Status.text(Status.not_found()) == "Not Found"
assert Status.text(Status.teapot()) == "I'm a teapot"
assert Status.text(Status.from_code(299)) == ""
Status.continue_
Section titled “Status.continue_”fn continue_(): Status
type function on Status
100 Continue.
Interactive Tests
assert Status.continue_() == Status.from_code(100)
assert Status.text(Status.continue_()) == "Continue"
Status.switching_protocols
Section titled “Status.switching_protocols”fn switching_protocols(): Status
type function on Status
101 Switching Protocols.
Status.processing
Section titled “Status.processing”fn processing(): Status
type function on Status
102 Processing.
Status.early_hints
Section titled “Status.early_hints”fn early_hints(): Status
type function on Status
103 Early Hints.
Status.ok
Section titled “Status.ok”fn ok(): Status
type function on Status
200 OK.
Interactive Tests
assert Status.ok() == Status.from_code(200)
assert Status.text(Status.ok()) == "OK"
Status.created
Section titled “Status.created”fn created(): Status
type function on Status
201 Created.
Interactive Tests
assert Status.created() == Status.from_code(201)
assert Status.text(Status.created()) == "Created"
Status.accepted
Section titled “Status.accepted”fn accepted(): Status
type function on Status
202 Accepted.
Status.non_authoritative_info
Section titled “Status.non_authoritative_info”fn non_authoritative_info(): Status
type function on Status
203 Non-Authoritative Information.
Status.no_content
Section titled “Status.no_content”fn no_content(): Status
type function on Status
204 No Content.
Interactive Tests
assert Status.no_content() == Status.from_code(204)
assert Status.text(Status.no_content()) == "No Content"
Status.reset_content
Section titled “Status.reset_content”fn reset_content(): Status
type function on Status
205 Reset Content.
Status.partial_content
Section titled “Status.partial_content”fn partial_content(): Status
type function on Status
206 Partial Content.
Status.multi_status
Section titled “Status.multi_status”fn multi_status(): Status
type function on Status
207 Multi-Status.
Status.already_reported
Section titled “Status.already_reported”fn already_reported(): Status
type function on Status
208 Already Reported.
Status.im_used
Section titled “Status.im_used”fn im_used(): Status
type function on Status
226 IM Used.
Status.multiple_choices
Section titled “Status.multiple_choices”fn multiple_choices(): Status
type function on Status
300 Multiple Choices.
Status.moved_permanently
Section titled “Status.moved_permanently”fn moved_permanently(): Status
type function on Status
301 Moved Permanently.
Interactive Tests
assert Status.moved_permanently() == Status.from_code(301)
assert Status.text(Status.moved_permanently()) == "Moved Permanently"
Status.found
Section titled “Status.found”fn found(): Status
type function on Status
302 Found.
Status.see_other
Section titled “Status.see_other”fn see_other(): Status
type function on Status
303 See Other.
Status.not_modified
Section titled “Status.not_modified”fn not_modified(): Status
type function on Status
304 Not Modified.
Interactive Tests
assert Status.not_modified() == Status.from_code(304)
assert Status.text(Status.not_modified()) == "Not Modified"
Status.use_proxy
Section titled “Status.use_proxy”fn use_proxy(): Status
type function on Status
305 Use Proxy.
Status.temporary_redirect
Section titled “Status.temporary_redirect”fn temporary_redirect(): Status
type function on Status
307 Temporary Redirect.
Status.permanent_redirect
Section titled “Status.permanent_redirect”fn permanent_redirect(): Status
type function on Status
308 Permanent Redirect.
Status.bad_request
Section titled “Status.bad_request”fn bad_request(): Status
type function on Status
400 Bad Request.
Interactive Tests
assert Status.bad_request() == Status.from_code(400)
assert Status.text(Status.bad_request()) == "Bad Request"
Status.unauthorized
Section titled “Status.unauthorized”fn unauthorized(): Status
type function on Status
401 Unauthorized.
Interactive Tests
assert Status.unauthorized() == Status.from_code(401)
assert Status.text(Status.unauthorized()) == "Unauthorized"
Status.payment_required
Section titled “Status.payment_required”fn payment_required(): Status
type function on Status
402 Payment Required.
Status.forbidden
Section titled “Status.forbidden”fn forbidden(): Status
type function on Status
403 Forbidden.
Interactive Tests
assert Status.forbidden() == Status.from_code(403)
assert Status.text(Status.forbidden()) == "Forbidden"
Status.not_found
Section titled “Status.not_found”fn not_found(): Status
type function on Status
404 Not Found.
Interactive Tests
assert Status.not_found() == Status.from_code(404)
assert Status.text(Status.not_found()) == "Not Found"
Status.method_not_allowed
Section titled “Status.method_not_allowed”fn method_not_allowed(): Status
type function on Status
405 Method Not Allowed.
Interactive Tests
assert Status.method_not_allowed() == Status.from_code(405)
assert Status.text(Status.method_not_allowed()) == "Method Not Allowed"
Status.not_acceptable
Section titled “Status.not_acceptable”fn not_acceptable(): Status
type function on Status
406 Not Acceptable.
Status.proxy_auth_required
Section titled “Status.proxy_auth_required”fn proxy_auth_required(): Status
type function on Status
407 Proxy Authentication Required.
Status.request_timeout
Section titled “Status.request_timeout”fn request_timeout(): Status
type function on Status
408 Request Timeout.
Status.conflict
Section titled “Status.conflict”fn conflict(): Status
type function on Status
409 Conflict.
Interactive Tests
assert Status.conflict() == Status.from_code(409)
assert Status.text(Status.conflict()) == "Conflict"
Status.gone
Section titled “Status.gone”fn gone(): Status
type function on Status
410 Gone.
Status.length_required
Section titled “Status.length_required”fn length_required(): Status
type function on Status
411 Length Required.
Status.precondition_failed
Section titled “Status.precondition_failed”fn precondition_failed(): Status
type function on Status
412 Precondition Failed.
Status.request_entity_too_large
Section titled “Status.request_entity_too_large”fn request_entity_too_large(): Status
type function on Status
413 Request Entity Too Large.
Status.request_uri_too_long
Section titled “Status.request_uri_too_long”fn request_uri_too_long(): Status
type function on Status
414 Request URI Too Long.
Status.unsupported_media_type
Section titled “Status.unsupported_media_type”fn unsupported_media_type(): Status
type function on Status
415 Unsupported Media Type.
Status.requested_range_not_satisfiable
Section titled “Status.requested_range_not_satisfiable”fn requested_range_not_satisfiable(): Status
type function on Status
416 Requested Range Not Satisfiable.
Status.expectation_failed
Section titled “Status.expectation_failed”fn expectation_failed(): Status
type function on Status
417 Expectation Failed.
Status.teapot
Section titled “Status.teapot”fn teapot(): Status
type function on Status
418 I'm a teapot.
Interactive Tests
assert Status.teapot() == Status.from_code(418)
assert Status.text(Status.teapot()) == "I'm a teapot"
Status.misdirected_request
Section titled “Status.misdirected_request”fn misdirected_request(): Status
type function on Status
421 Misdirected Request.
Status.unprocessable_entity
Section titled “Status.unprocessable_entity”fn unprocessable_entity(): Status
type function on Status
422 Unprocessable Entity.
Interactive Tests
assert Status.unprocessable_entity() == Status.from_code(422)
assert Status.text(Status.unprocessable_entity()) == "Unprocessable Entity"
Status.locked
Section titled “Status.locked”fn locked(): Status
type function on Status
423 Locked.
Status.failed_dependency
Section titled “Status.failed_dependency”fn failed_dependency(): Status
type function on Status
424 Failed Dependency.
Status.too_early
Section titled “Status.too_early”fn too_early(): Status
type function on Status
425 Too Early.
Status.upgrade_required
Section titled “Status.upgrade_required”fn upgrade_required(): Status
type function on Status
426 Upgrade Required.
Status.precondition_required
Section titled “Status.precondition_required”fn precondition_required(): Status
type function on Status
428 Precondition Required.
Status.too_many_requests
Section titled “Status.too_many_requests”fn too_many_requests(): Status
type function on Status
429 Too Many Requests.
Interactive Tests
assert Status.too_many_requests() == Status.from_code(429)
assert Status.text(Status.too_many_requests()) == "Too Many Requests"
Status.request_header_fields_too_large
Section titled “Status.request_header_fields_too_large”fn request_header_fields_too_large(): Status
type function on Status
431 Request Header Fields Too Large.
Status.unavailable_for_legal_reasons
Section titled “Status.unavailable_for_legal_reasons”fn unavailable_for_legal_reasons(): Status
type function on Status
451 Unavailable For Legal Reasons.
Status.internal_server_error
Section titled “Status.internal_server_error”fn internal_server_error(): Status
type function on Status
500 Internal Server Error.
Interactive Tests
assert Status.internal_server_error() == Status.from_code(500)
assert Status.text(Status.internal_server_error()) == "Internal Server Error"
Status.not_implemented
Section titled “Status.not_implemented”fn not_implemented(): Status
type function on Status
501 Not Implemented.
Interactive Tests
assert Status.not_implemented() == Status.from_code(501)
assert Status.text(Status.not_implemented()) == "Not Implemented"
Status.bad_gateway
Section titled “Status.bad_gateway”fn bad_gateway(): Status
type function on Status
502 Bad Gateway.
Status.service_unavailable
Section titled “Status.service_unavailable”fn service_unavailable(): Status
type function on Status
503 Service Unavailable.
Interactive Tests
assert Status.service_unavailable() == Status.from_code(503)
assert Status.text(Status.service_unavailable()) == "Service Unavailable"
Status.gateway_timeout
Section titled “Status.gateway_timeout”fn gateway_timeout(): Status
type function on Status
504 Gateway Timeout.
Interactive Tests
assert Status.gateway_timeout() == Status.from_code(504)
assert Status.text(Status.gateway_timeout()) == "Gateway Timeout"
Status.http_version_not_supported
Section titled “Status.http_version_not_supported”fn http_version_not_supported(): Status
type function on Status
505 HTTP Version Not Supported.
Status.variant_also_negotiates
Section titled “Status.variant_also_negotiates”fn variant_also_negotiates(): Status
type function on Status
506 Variant Also Negotiates.
Status.insufficient_storage
Section titled “Status.insufficient_storage”fn insufficient_storage(): Status
type function on Status
507 Insufficient Storage.
Status.loop_detected
Section titled “Status.loop_detected”fn loop_detected(): Status
type function on Status
508 Loop Detected.
Status.not_extended
Section titled “Status.not_extended”fn not_extended(): Status
type function on Status
510 Not Extended.
Status.network_authentication_required
Section titled “Status.network_authentication_required”fn network_authentication_required(): Status
type function on Status
511 Network Authentication Required.
Status.equal?
Section titled “Status.equal?”fn equal?(a: Status, b: Status): Bool
impl Equatable.equal?
Two statuses are equal when their codes are. Hand-written for the same
reason as Method’s: == on a struct dispatches here, so the explicit
and the operator forms cannot disagree.
Interactive Tests
assert Equatable.equal?(Status.from_code(200), Status.ok())
refute Equatable.equal?(Status.ok(), Status.created())
Status.to_string
Section titled “Status.to_string”fn to_string(status: Status): String
impl Display.to_string
The numeric code.
Interactive Tests
assert Display.to_string(Status.not_found()) == "404"
Status.inspect
Section titled “Status.inspect”fn inspect(status: Status): String
impl Debug.inspect
Interactive Tests
assert Debug.inspect(Status.ok()) == "Status(200 OK)"
assert Debug.inspect(Status.from_code(299)) == "Status(299 )"
Status.hash
Section titled “Status.hash”fn hash(value: Status): Int
impl Hashable.hash
enum Error
Section titled “enum Error”enum Error { InvalidRequest { reason: String } Transport { reason: String } Body { reason: String } ServerFailure { reason: String } }
Distinguishable failures returned by the HTTP adapter.
Every variant has a construction path: InvalidRequest when a method or
URL cannot form a request, Transport when the round trip fails,
Body when a body cannot be read or decoded, and ServerFailure when
binding, serving or shutting down fails.
Interactive Tests
assert Display.to_string(
Error.Transport{reason: "refused"}
) == "HTTP transport error: refused"
refute Error.Body{reason: "eof"} == Error.Transport{reason: "eof"}
Error.to_string
Section titled “Error.to_string”fn to_string(e: Error): String
impl Display.to_string
Interactive Tests
assert Display.to_string(
Error.InvalidRequest{reason: "bad URL"}
) == "invalid HTTP request: bad URL"
assert Display.to_string(
Error.Body{reason: "not UTF-8"}
) == "HTTP body error: not UTF-8"
assert Display.to_string(
Error.ServerFailure{reason: "bind failed"}
) == "HTTP server error: bind failed"
Error.inspect
Section titled “Error.inspect”fn inspect(value: Error): String
impl Debug.inspect
Error.equal?
Section titled “Error.equal?”fn equal?(a: Error, b: Error): Bool
impl Equatable.equal?
struct Header
Section titled “struct Header”struct Header
opaque — construction surface is private to its defining module
HTTP header fields: an ordered list of (name, value) pairs where a name
may repeat.
Field names are case-insensitive (RFC 9110 §5.1), so Header stores and
compares them in the single canonical form Header.canonical produces.
The canonicalisation is Nomi’s own and explicit — it is not Go’s
http.Header, which is what previously leaked name-mangling into
case-sensitive query parameters.
get distinguishes absent from present-but-empty: a field set to ""
answers Maybe.Some("") and has? answers True.
Interactive Tests
headers = Header.from_list([("content-type", "text/plain"), ("Tag", "a")])
assert Header.get(headers, "CONTENT-TYPE") == Maybe.Some("text/plain")
assert Header.names(headers) == ["Content-Type", "Tag"]
assert Header.has?(headers, "tag")
refute Query.has?(Query.from_list([("Tag", "a")]), "tag")
Header.empty
Section titled “Header.empty”fn empty(): Header
type function on Header
Headers with no fields.
Interactive Tests
refute Header.has?(Header.empty(), "Content-Type")
assert Header.get(Header.empty(), "Content-Type") == Maybe.None
assert Header.names(Header.empty()) == []
Header.from_list
Section titled “Header.from_list”fn from_list(entries: List<(String, String)>): Header
type function on Header
Headers from (name, value) pairs. Names are canonicalised; repeated
names keep every value, in the order given.
Interactive Tests
headers = Header.from_list([("set-cookie", "a=1"), ("SET-COOKIE", "b=2")])
assert Header.to_list(headers) == [("Set-Cookie", "a=1"), ("Set-Cookie", "b=2")]
assert Header.values(headers, "Set-Cookie") == ["a=1", "b=2"]
Header.to_list
Section titled “Header.to_list”fn to_list(headers: Header): List<(String, String)>
type function on Header
Every field as a (canonical name, value) pair, in order.
Interactive Tests
headers = Header.set(Header.empty(), "content-length", "0")
assert Header.to_list(headers) == [("Content-Length", "0")]
Header.canonical
Section titled “Header.canonical”fn canonical(name: String): String
type function on Header
The canonical form of a field name: content-type becomes
Content-Type. Each --separated token is title-cased, matching Go’s
textproto.CanonicalMIMEHeaderKey.
Interactive Tests
assert Header.canonical("content-type") == "Content-Type"
assert Header.canonical("X-NOMI-TRACE") == "X-Nomi-Trace"
assert Header.canonical("Content-Type") == "Content-Type"
assert Header.canonical("") == ""
Header.get
Section titled “Header.get”fn get(headers: Header, name: String): Maybe<String>
type function on Header
The first value for name, or Maybe.None when the field is absent.
A field whose value is "" is present, so this answers
Maybe.Some("") rather than Maybe.None.
Interactive Tests
headers = Header.set(Header.empty(), "X-Trace", "")
assert Header.get(headers, "x-trace") == Maybe.Some("")
assert Header.has?(headers, "x-trace")
assert Header.get(headers, "X-Other") == Maybe.None
refute Header.has?(headers, "X-Other")
Header.values
Section titled “Header.values”fn values(headers: Header, name: String): List<String>
type function on Header
Every value for name, in order.
Interactive Tests
headers =
Header.empty()
|> Header.add("accept", "text/html")
|> Header.add("ACCEPT", "application/json")
assert Header.values(headers, "Accept") == ["text/html", "application/json"]
assert Header.values(headers, "Host") == []
Header.names
Section titled “Header.names”fn names(headers: Header): List<String>
type function on Header
Every field name present, canonicalised, each once, in first-appearance order.
Interactive Tests
headers =
Header.empty()
|> Header.add("accept", "text/html")
|> Header.add("ACCEPT", "application/json")
|> Header.add("host", "example.com")
assert Header.names(headers) == ["Accept", "Host"]
Header.set
Section titled “Header.set”fn set(headers: Header, name: String, value: String): Header
type function on Header
Headers where name has exactly value, replacing any existing values
for that name and appending if it was absent.
Interactive Tests
headers =
Header.empty()
|> Header.add("accept", "text/html")
|> Header.add("accept", "application/json")
|> Header.set("ACCEPT", "*/*")
assert Header.values(headers, "Accept") == ["*/*"]
Header.add
Section titled “Header.add”fn add(headers: Header, name: String, value: String): Header
type function on Header
Headers with value appended to name, keeping any existing values.
Interactive Tests
headers =
Header.empty()
|> Header.set("accept", "text/html")
|> Header.add("accept", "application/json")
assert Header.values(headers, "Accept") == ["text/html", "application/json"]
Header.delete
Section titled “Header.delete”fn delete(headers: Header, name: String): Header
type function on Header
Headers without any field named name.
Interactive Tests
headers = Header.set(Header.empty(), "X-Trace", "abc")
refute Header.has?(Header.delete(headers, "x-trace"), "X-Trace")
assert Header.has?(Header.delete(headers, "X-Other"), "X-Trace")
Header.has?
Section titled “Header.has?”fn has?(headers: Header, name: String): Bool
type function on Header
True when a field named name is present, whatever its value.
Interactive Tests
headers = Header.set(Header.empty(), "X-Trace", "")
assert Header.has?(headers, "x-TRACE")
refute Header.has?(headers, "X-Missing")
Header.inspect
Section titled “Header.inspect”fn inspect(value: Header): String
impl Debug.inspect
struct Query
Section titled “struct Query”struct Query
opaque — construction surface is private to its defining module
URL query parameters: an ordered list of (name, value) pairs where a
name may repeat.
Query parameters are case-SENSITIVE and their order is observable, which
is why they are their own type rather than a Header. ?userId=7&Tag=a&tag=b
round-trips as [("userId", "7"), ("Tag", "a"), ("tag", "b")]; passing it
through a case-insensitive header map merges Tag with tag and rewrites
userId, silently corrupting the data.
Interactive Tests
query = Query.parse("?userId=7&Tag=a&tag=b")
assert Query.to_list(query) == [("userId", "7"), ("Tag", "a"), ("tag", "b")]
assert Query.get(query, "Tag") == Maybe.Some("a")
assert Query.get(query, "tag") == Maybe.Some("b")
refute Query.has?(query, "userid")
assert Header.has?(Header.from_list([("userId", "7")]), "userid")
Query.empty
Section titled “Query.empty”fn empty(): Query
type function on Query
Query parameters with no entries.
Interactive Tests
refute Query.has?(Query.empty(), "q")
assert Query.encode(Query.empty()) == ""
assert Query.to_list(Query.empty()) == []
Query.parse
Section titled “Query.parse”fn parse(raw: String): Query
type function on Query
Parse a raw query string, percent-decoding each name and value and
keeping the wire order and the wire spelling. A leading ? is
tolerated.
Interactive Tests
assert Query.to_list(Query.parse("?q=a+b")) == [("q", "a b")]
assert Query.to_list(Query.parse("q=a%20b")) == [("q", "a b")]
assert Query.to_list(Query.parse("")) == []
assert Query.to_list(Query.parse("flag")) == [("flag", "")]
Query.from_list
Section titled “Query.from_list”fn from_list(entries: List<(String, String)>): Query
type function on Query
Query parameters from (name, value) pairs, verbatim.
Interactive Tests
query = Query.from_list([("Tag", "a"), ("tag", "b")])
assert Query.to_list(query) == [("Tag", "a"), ("tag", "b")]
assert Query.names(query) == ["Tag", "tag"]
Query.to_list
Section titled “Query.to_list”fn to_list(query: Query): List<(String, String)>
type function on Query
Every parameter as a (name, value) pair, in order.
Interactive Tests
assert Query.to_list(Query.from_list([("q", "1")])) == [("q", "1")]
Query.get
Section titled “Query.get”fn get(query: Query, name: String): Maybe<String>
type function on Query
The first value for name, or Maybe.None when absent. A parameter
present with an empty value answers Maybe.Some("").
Interactive Tests
query = Query.parse("a=&b=2")
assert Query.get(query, "a") == Maybe.Some("")
assert Query.get(query, "b") == Maybe.Some("2")
assert Query.get(query, "A") == Maybe.None
assert Query.get(query, "c") == Maybe.None
Query.values
Section titled “Query.values”fn values(query: Query, name: String): List<String>
type function on Query
Every value for name, in order.
Interactive Tests
query = Query.parse("tag=a&Tag=b&tag=c")
assert Query.values(query, "tag") == ["a", "c"]
assert Query.values(query, "Tag") == ["b"]
Query.names
Section titled “Query.names”fn names(query: Query): List<String>
type function on Query
Every parameter name present, each once, in first-appearance order.
Interactive Tests
assert Query.names(Query.parse("a=1&b=2&a=3")) == ["a", "b"]
Query.has?
Section titled “Query.has?”fn has?(query: Query, name: String): Bool
type function on Query
True when a parameter named name is present, whatever its value.
Interactive Tests
assert Query.has?(Query.parse("flag"), "flag")
refute Query.has?(Query.parse("flag"), "FLAG")
Query.encode
Section titled “Query.encode”fn encode(query: Query): String
type function on Query
Render as a raw query string, percent-encoding each name and value and preserving order.
Interactive Tests
assert Query.encode(Query.from_list([("q", "a b"), ("q", "c")])) == "q=a+b&q=c"
assert Query.encode(Query.parse("?userId=7&Tag=a")) == "userId=7&Tag=a"
Query.to_string
Section titled “Query.to_string”fn to_string(query: Query): String
impl Display.to_string
The encoded query string.
Interactive Tests
assert Display.to_string(Query.from_list([("q", "a b")])) == "q=a+b"
Query.inspect
Section titled “Query.inspect”fn inspect(value: Query): String
impl Debug.inspect
enum SameSite
Section titled “enum SameSite”enum SameSite { Unset Lax Strict None }
The SameSite attribute of a cookie. Unset omits the attribute.
Interactive Tests
assert Display.to_string(SameSite.Unset) == ""
assert Display.to_string(SameSite.Lax) == "Lax"
assert Display.to_string(SameSite.Strict) == "Strict"
assert Display.to_string(SameSite.None) == "None"
SameSite.to_string
Section titled “SameSite.to_string”fn to_string(same_site: SameSite): String
impl Display.to_string
The wire spelling, or "" for Unset.
Interactive Tests
assert Display.to_string(SameSite.Unset) == ""
assert Display.to_string(SameSite.Strict) == "Strict"
SameSite.inspect
Section titled “SameSite.inspect”fn inspect(value: SameSite): String
impl Debug.inspect
SameSite.equal?
Section titled “SameSite.equal?”fn equal?(a: SameSite, b: SameSite): Bool
impl Equatable.equal?
struct Cookie
Section titled “struct Cookie”struct Cookie { name: String value: String path: String domain: String max_age: Maybe<Int> secure: Bool http_only: Bool same_site: SameSite }
An HTTP cookie, with the attributes a server actually needs to set one.
path and domain are omitted from the wire form when ""; max_age
is omitted when Maybe.None. Expires is deliberately absent: RFC 6265
§4.1.2.2 prefers Max-Age, and rendering Expires needs IMF-fixdate
formatting, which would pull std/calendar into std/http for a strictly
weaker attribute.
Interactive Tests
assert Display.to_string(Cookie.new("session", "abc")) == "session=abc"
cookie = Struct.update(Cookie.new("session", "abc"), {
path: "/",
domain: "example.com",
max_age: Maybe.Some(3600),
secure: True,
http_only: True,
same_site: SameSite.Lax,
})
assert Display.to_string(
cookie
) == "session=abc; Path=/; Domain=example.com; Max-Age=3600; Secure; HttpOnly; SameSite=Lax"
assert Cookie.parse(Display.to_string(cookie)) == Maybe.Some(cookie)
Cookie.new
Section titled “Cookie.new”fn new(name: String, value: String): Cookie
type function on Cookie
A cookie with a name and a value and no attributes. Display renders
it as exactly name=value.
Interactive Tests
assert Display.to_string(Cookie.new("session", "abc")) == "session=abc"
assert Cookie.new("session", "abc").max_age == Maybe.None
assert Cookie.new("session", "abc").same_site == SameSite.Unset
refute Cookie.new("session", "abc").secure
Cookie.parse
Section titled “Cookie.parse”fn parse(raw: String): Maybe<Cookie>
type function on Cookie
Parse one Set-Cookie field value. Unknown attributes are ignored;
a field with no name is rejected.
Interactive Tests
assert Cookie.parse("session=abc") == Maybe.Some(Cookie.new("session", "abc"))
assert Cookie.parse("a=1; Frobnicate=yes") == Maybe.Some(Cookie.new("a", "1"))
assert Cookie.parse("=nope") == Maybe.None
assert Cookie.parse(
"session=abc; Path=/; Domain=example.com; Max-Age=3600; Secure; HttpOnly; SameSite=Lax"
) == Maybe.Some(Struct.update(Cookie.new("session", "abc"), {
path: "/",
domain: "example.com",
max_age: Maybe.Some(3600),
secure: True,
http_only: True,
same_site: SameSite.Lax,
}))
Cookie.to_string
Section titled “Cookie.to_string”fn to_string(cookie: Cookie): String
impl Display.to_string
The Set-Cookie field value: name=value followed by every attribute
that is set, separated by ; .
Interactive Tests
assert Display.to_string(Cookie.new("session", "abc")) == "session=abc"
cookie = Struct.update(
Cookie.new("session", "abc"),
{path: "/", http_only: True},
)
assert Display.to_string(cookie) == "session=abc; Path=/; HttpOnly"
Cookie.inspect
Section titled “Cookie.inspect”fn inspect(cookie: Cookie): String
impl Debug.inspect
Interactive Tests
assert Debug.inspect(Cookie.new("session", "abc")) == "Cookie(session=abc)"
Cookie.equal?
Section titled “Cookie.equal?”fn equal?(a: Cookie, b: Cookie): Bool
impl Equatable.equal?
type Cancellation
Section titled “type Cancellation”type Cancellation Int
opaque — construction surface is private to its defining module
The cancellation signal of one in-flight server request.
Opaque, and cheap to copy: it addresses a signal the Go half owns rather
than holding one. Cancellation.fired? answers True once the peer that
sent the request has disconnected, or Server.shutdown has begun.
It is a value rather than a Context entry because Nomi’s Context
carries deadlines and values, not cancellation (see Request), and it is
a distinct type rather than a third host handle because the module’s
design is that only genuinely-effectful things — Client and Server —
are host handles.
Interactive Tests
refute Cancellation.fired?(Cancellation.never())
Cancellation.never
Section titled “Cancellation.never”fn never(): Cancellation
type function on Cancellation
The signal a client-built request carries: never fired, because nobody can hang up on a request you are about to send.
Interactive Tests
refute Cancellation.fired?(Cancellation.never())
Cancellation.fired?
Section titled “Cancellation.fired?”fn fired?(cancellation: Cancellation): Bool
type function on Cancellation
True once the request this signal belongs to has been cancelled.
Interactive Tests
refute Cancellation.fired?(Cancellation.never())
refute Request.canceled?(Request.new(Method.get(), "/"))
Cancellation.inspect
Section titled “Cancellation.inspect”fn inspect(value: Cancellation): String
impl Debug.inspect
struct Request
Section titled “struct Request”struct Request { method: Method url: String headers: Header body: Bytes context: Context cancellation: Cancellation }
An HTTP request as an immutable value.
context carries the request’s DEADLINE, and cancellation carries its
cancellation: for a request a server handler receives, cancellation
fires when the client disconnects or the server begins shutting down, and
an outbound Client call made with that request is cancelled along with
it, so an abandoned request stops doing work on the caller’s behalf.
The two are separate fields because Nomi’s Context is deadline-and-values
by design — value.ContextVal’s own contract is that “deadlines are the
only state a Context carries”, and a task being stopped “travels on the Go
context”. Cancellation is that Go-side signal, named as a Nomi value.
Cookies live in the headers, not in a second field: a request’s cookies
ARE its Cookie field, and keeping a parallel list is two sources of
truth for one thing. Use Request.cookie and Request.add_cookie.
Interactive Tests
request = Request.new(Method.get(), "/items?tag=a&tag=b")
assert Request.path(request) == "/items"
assert Query.values(Request.query(request), "tag") == ["a", "b"]
refute Request.canceled?(request)
Request.new
Section titled “Request.new”fn new(method: Method, url: String, headers: Header, body: Bytes, context: Context): Request
type function on Request
Build a request. The URL is not validated here — validation belongs to
the send, which is where a bad URL becomes Error.InvalidRequest,
rather than to a throwaway parse whose verdict may differ.
Interactive Tests
request = Request.new(Method.post(), "https://example.com/items?q=1")
assert request.method == Method.post()
assert Request.path(request) == "/items"
assert Request.header(request, "Content-Type") == Maybe.None
assert Context.deadline(request.context) == Maybe.None
refute Request.canceled?(request)
Request.with_context
Section titled “Request.with_context”fn with_context(request: Request, context: Context): Request
type function on Request
The same request under a different context.
Interactive Tests
deadline = Context.with_deadline(Context.root(), Instant.from_seconds(1))
request = Request.with_context(Request.new(Method.get(), "/"), deadline)
assert Context.deadline(request.context) == Maybe.Some(Instant.from_seconds(1))
Request.path
Section titled “Request.path”fn path(request: Request): String
type function on Request
The URL’s path, with no query string and no fragment.
Interactive Tests
assert Request.path(Request.new(Method.get(), "/a/b?q=1#frag")) == "/a/b"
assert Request.path(
Request.new(Method.get(), "https://example.com/a?q=1")
) == "/a"
Request.query
Section titled “Request.query”fn query(request: Request): Query
type function on Request
The URL’s query parameters, case-sensitively.
Interactive Tests
request = Request.new(Method.get(), "/items?userId=7&Tag=a&tag=b")
assert Query.to_list(Request.query(request)) == [
("userId", "7"),
("Tag", "a"),
("tag", "b"),
]
assert Query.get(Request.query(request), "tag") == Maybe.Some("b")
refute Query.has?(Request.query(request), "userid")
Request.header
Section titled “Request.header”fn header(request: Request, name: String): Maybe<String>
type function on Request
The first value of a header field, or Maybe.None when absent. A field
present with an empty value answers Maybe.Some("").
Interactive Tests
request = Request.set_header(Request.new(Method.get(), "/"), "X-Trace", "")
assert Request.header(request, "x-trace") == Maybe.Some("")
assert Request.header(request, "X-Other") == Maybe.None
Request.cookie
Section titled “Request.cookie”fn cookie(request: Request, name: String): Maybe<Cookie>
type function on Request
The cookie named name from the request’s Cookie field. Request
cookies carry no attributes, so only name and value are set.
Interactive Tests
request = Request.add_cookie(
Request.new(Method.get(), "/"),
Cookie.new("a", "1"),
)
assert Request.cookie(request, "a") == Maybe.Some(Cookie.new("a", "1"))
assert Request.cookie(request, "z") == Maybe.None
assert Request.cookie(Request.new(Method.get(), "/"), "a") == Maybe.None
Request.set_header
Section titled “Request.set_header”fn set_header(request: Request, name: String, value: String): Request
type function on Request
The same request with a header field set to exactly value.
Interactive Tests
request =
Request.new(Method.get(), "/")
|> Request.add_header("accept", "text/html")
|> Request.set_header("ACCEPT", "*/*")
assert Header.values(request.headers, "Accept") == ["*/*"]
Request.add_header
Section titled “Request.add_header”fn add_header(request: Request, name: String, value: String): Request
type function on Request
The same request with value appended to a header field.
Interactive Tests
request =
Request.new(Method.get(), "/")
|> Request.add_header("accept", "text/html")
|> Request.add_header("accept", "application/json")
assert Header.values(request.headers, "Accept") == [
"text/html",
"application/json",
]
Request.add_cookie
Section titled “Request.add_cookie”fn add_cookie(request: Request, cookie: Cookie): Request
type function on Request
The same request with a cookie appended to its Cookie field.
Interactive Tests
request =
Request.new(Method.get(), "/")
|> Request.add_cookie(Cookie.new("a", "1"))
|> Request.add_cookie(Cookie.new("b", "2"))
assert Request.header(request, "Cookie") == Maybe.Some("a=1; b=2")
assert Request.cookie(request, "b") == Maybe.Some(Cookie.new("b", "2"))
Request.canceled?
Section titled “Request.canceled?”fn canceled?(request: Request): Bool
type function on Request
True once the peer that sent this request has gone away, or the
server it arrived at has begun shutting down.
Always False for a request a client built: nobody can hang up on it.
Interactive Tests
refute Request.canceled?(Request.new(Method.get(), "/"))
Request.inspect
Section titled “Request.inspect”fn inspect(value: Request): String
impl Debug.inspect
struct Response
Section titled “struct Response”struct Response { status: Status headers: Header body: Bytes }
An HTTP response as an immutable value.
This is the type a client receives AND the type a server handler returns. There is no separate handler-response type: they were near-identical, and a handler that cannot produce the value a client observes cannot be tested against one.
Interactive Tests
response = Response.text(Status.ok(), "hi")
assert response.status == Status.from_code(200)
assert Response.body_text(response) == Ok("hi")
assert Response.status_text(response) == "OK"
Response.new
Section titled “Response.new”fn new(status: Status, headers: Header, body: Bytes): Response
type function on Response
Build a response.
Interactive Tests
response = Response.new(Status.no_content())
assert response.status == Status.from_code(204)
assert Response.body_text(response) == Ok("")
assert Response.header(response, "Content-Type") == Maybe.None
Response.text
Section titled “Response.text”fn text(status: Status, body: String): Response
type function on Response
A text/plain; charset=utf-8 response carrying body.
Interactive Tests
response = Response.text(Status.not_found(), "nope")
assert Response.body_text(response) == Ok("nope")
assert Response.header(response, "content-type") == Maybe.Some(
"text/plain; charset=utf-8"
)
Response.status_text
Section titled “Response.status_text”fn status_text(response: Response): String
type function on Response
The reason phrase for the response’s status.
Interactive Tests
assert Response.status_text(Response.new(Status.teapot())) == "I'm a teapot"
assert Response.status_text(Response.new(Status.from_code(299))) == ""
Response.header
Section titled “Response.header”fn header(response: Response, name: String): Maybe<String>
type function on Response
The first value of a header field, or Maybe.None when absent. A field
present with an empty value answers Maybe.Some("").
Interactive Tests
response = Response.with_header(Response.new(Status.ok()), "X-Trace", "")
assert Response.header(response, "x-trace") == Maybe.Some("")
assert Response.header(response, "X-Other") == Maybe.None
Response.body_text
Section titled “Response.body_text”fn body_text(response: Response): Result<String, Error>
type function on Response
The body decoded as UTF-8 text.
Interactive Tests
assert Response.body_text(Response.text(Status.ok(), "hi")) == Ok("hi")
assert Response.body_text(Response.new(Status.no_content())) == Ok("")
Response.cookies
Section titled “Response.cookies”fn cookies(response: Response): List<Cookie>
type function on Response
Every cookie the response sets, parsed from its Set-Cookie fields.
Fields that do not parse are skipped.
Interactive Tests
response =
Response.new(Status.ok())
|> Response.with_cookie(Cookie.new("a", "1"))
|> Response.with_cookie(Cookie.new("b", "2"))
assert Response.cookies(response) == [
Cookie.new("a", "1"),
Cookie.new("b", "2"),
]
assert Response.cookies(Response.new(Status.ok())) == []
Response.with_header
Section titled “Response.with_header”fn with_header(response: Response, name: String, value: String): Response
type function on Response
The same response with a header field set to exactly value.
Interactive Tests
response =
Response.new(Status.ok())
|> Response.with_header("x-trace", "abc")
|> Response.with_header("X-TRACE", "def")
assert Header.values(response.headers, "X-Trace") == ["def"]
Response.with_cookie
Section titled “Response.with_cookie”fn with_cookie(response: Response, cookie: Cookie): Response
type function on Response
The same response with one more Set-Cookie field.
Interactive Tests
cookie = Struct.update(
Cookie.new("session", "abc"),
{path: "/", http_only: True},
)
response = Response.with_cookie(Response.new(Status.ok()), cookie)
assert Response.header(response, "Set-Cookie") == Maybe.Some(
"session=abc; Path=/; HttpOnly"
)
assert Response.cookies(response) == [cookie]
Response.inspect
Section titled “Response.inspect”fn inspect(value: Response): String
impl Debug.inspect
type Client
Section titled “type Client”type Client
opaque — construction surface is private to its defining module
An HTTP client. One of the module’s two host handles, because it owns connection pooling and sockets.
Client.default
Section titled “Client.default”fn default(): Client
type function on Client
A client with no client-level timeout. Bound individual calls with a
Context deadline instead — that is the bound that composes with the
deadline a caller was already under.
Client.with_timeout
Section titled “Client.with_timeout”fn with_timeout(timeout: Duration): Client
type function on Client
A client whose every call is bounded by timeout, covering connection
setup, the round trip, and reading the body.
Client.send
Section titled “Client.send”fn send(client: Client, request: Request): Result<Response, Error>
type function on Client
Send a request and buffer the whole response.
The deadline and cancellation come from request.context, which is the
field that exists to carry them; use Request.with_context to change
them.
Client.get
Section titled “Client.get”fn get(client: Client, context: Context, url: String): Result<Response, Error>
type function on Client
GET url under context.
Client.post
Section titled “Client.post”fn post(client: Client, context: Context, url: String, content_type: String, body: Bytes): Result<Response, Error>
type function on Client
POST url under context, with an explicit content type.
The content type is a parameter rather than a hardcoded
application/octet-stream: a client that cannot say what it is sending
cannot post a form or a JSON document.
Client.inspect
Section titled “Client.inspect”fn inspect(value: Client): String
impl Debug.inspect
type Server
Section titled “type Server”type Server
opaque — construction surface is private to its defining module
A listening HTTP server. The module’s other host handle: it owns the listening socket and the accept loop.
Server.serve
Section titled “Server.serve”fn serve(addr: String, handler: (Request) -> Response): Result<Server, Error>
type function on Server
Bind addr, start accepting, and return immediately.
Handlers run concurrently — one goroutine per request, with no
serialising lock. Use Server.wait to block until the server stops, or
Server.run to do both in one call.
A handler returns a Response, the same type a client receives. It has
no failure channel because HTTP already has one: answer
Status.internal_server_error().
Server.run
Section titled “Server.run”fn run(addr: String, handler: (Request) -> Response): Result<Unit, Error>
type function on Server
Bind addr and serve until the server stops. Blocks.
Server.addr
Section titled “Server.addr”fn addr(server: Server): String
type function on Server
The address actually bound, which differs from the requested one
whenever the port was 0.
Server.wait
Section titled “Server.wait”fn wait(server: Server): Result<Unit, Error>
type function on Server
Block until the server has stopped serving.
Callable from several places at once; every caller sees the same
outcome. A server stopped by Server.shutdown finishes with Ok.
Server.shutdown
Section titled “Server.shutdown”fn shutdown(server: Server, context: Context): Result<Unit, Error>
type function on Server
Cancel every in-flight request’s context, then stop accepting and drain
until context’s deadline.
Cancelling first is deliberate: Go’s own Server.Shutdown leaves
request contexts alive, so a handler cannot tell the process is going
away. This module promises that a handler’s context cancels on
shutdown, so Request.canceled? becomes True before the drain.
Server.inspect
Section titled “Server.inspect”fn inspect(value: Server): String
impl Debug.inspect