Skip to content

http — transport-free HTTP/1.x primitives

Pure HTTP logic with no sockets, no I/O, and no aoughwl-substrate dependency: typed methods and status codes, header primitives, RFC 3986 URL/query/form codecs, tolerant request and response parsing, response building, and a chunked-transfer codec. It sits between tcp/tls and any server or client — serve is built on it — so the same HTTP layer backs any transport. Standard-library only; the optional http/contentcoding submodule adds Content-Encoding negotiation and pulls in the compress package.

Status — Production-ready and complete for HTTP/1.x message handling, one-shot and streaming. Everything is status-based (nothing raises) and operates on caller-owned strings; parseRequest is tolerant rather than validating (malformed input yields empty fields, not errors). Incremental parsing is now available: StreamParser (http/stream) resumes across chunk boundaries so a transport can feed bytes as they arrive without buffering the whole message first.

Quickstart

nim
import http

# Parse a raw request off the wire, dispatch, and build a reply.
let req = parseRequest(rawBytes)
if req.isMethod(HttpGet) and req.path.pathOnly == "/hello":
  let who = req.path.queryParam("name")   # "/hello?name=ada" -> "ada"
  let body = "hi " & (if who.len > 0: who else: "world")
  send responseToString(response(200, "text/plain", body))
else:
  send httpResponse(404, "text/plain", "not found")

API

import http re-exports the headers, url, request, response, httpmethod, and stream submodules. import http/contentcoding is separate and opt-in.

Types

symbolsignaturewhat it does
Headerobject with name*, value*: stringOne header field; name matched case-insensitively, original spelling preserved.
Requestobject with meth*, path*, version*, body*: string; headers*: seq[Header]Parsed HTTP/1.x request. path is the raw request-target (may include ?query).
Responseobject with status*: int; contentType*, body*: string; headers*: seq[Header]In-memory response model consumed by responseToString.
HttpMethodenum HttpUnknown, HttpGet, HttpHead, HttpPost, HttpPut, HttpDelete, HttpConnect, HttpOptions, HttpTrace, HttpPatchTyped request method (RFC 7231 / RFC 5789). HttpUnknown is the parse miss.
HttpCodedistinct intTyped status code with class predicates and a reason-phrase $.

Methods

symbolsignaturewhat it does
toStringproc(m: HttpMethod): stringCanonical upper-case token ("GET"…); "" for HttpUnknown.
`$`proc(m: HttpMethod): stringAlias for toString.
parseHttpMethodproc(s: string): HttpMethodTolerant, case-insensitive parse; unrecognized tokens map to HttpUnknown.
isMethodproc(req: Request; m: HttpMethod): boolTyped method check against a parsed request.

Headers

symbolsignaturewhat it does
headerproc(name, value: string): HeaderConstruct a Header.
headerValueproc(headers: seq[Header]; name: string): stringFirst matching value (case-insensitive), or "".
hasHeaderproc(headers: seq[Header]; name: string): boolWhether a non-empty value exists for name.
lowerAsciiproc(s: string): stringASCII-only lower-casing.
eqIgnoreCaseproc(a, b: string): boolASCII case-insensitive equality.
trimHttpproc(s: string): stringTrim leading/trailing spaces and horizontal tabs.

Request parsing

symbolsignaturewhat it does
parseRequestproc(raw: string): RequestParse request line, headers, and post-blank-line body. Malformed input yields empty fields, never raises.
isValidRequestproc(req: Request): boolTrue when both meth and path are non-empty.
isMethodproc(req: Request; meth: string): boolString method check (case-insensitive).
headerValueproc(req: Request; name: string): stringConvenience over req.headers.
hasHeaderproc(req: Request; name: string): boolConvenience over req.headers.

Streaming / incremental parsing — StreamParser

The one-shot parseRequest needs the whole message as a string. StreamParser (from the re-exported http/stream) is the resumable counterpart: create one, feed it received bytes as they arrive — a single byte, a whole message, or any chunk in between — and it advances through the request/status line, header block, and body, tolerating a split at any byte boundary (mid-line, mid-header, mid-chunk-size, mid-body). It supports Content-Length, Transfer-Encoding: chunked (de-chunked in place), and read-until-close response bodies. It reuses the same header primitives and Request/Response types as the one-shot API — toRequest / toResponse yield objects identical to parseRequest's.

feed returns how many bytes it consumed, so the caller keeps ownership of anything past the end of a message (HTTP pipelining) and the parser never over-reads. Limits (max line, max header block) surface as a status-based errorStatus; nothing raises.

nim
import http

var p = newRequestParser()
while p.needMore() and not p.isError():
  let chunk = transport.recvSome()        # any-size slice off the wire
  let used = p.feed(chunk)                 # used < chunk.len only at message end
  process p.takeBody()                     # stream the body out incrementally
if p.isComplete():
  let req = p.toRequest()                  # same shape as parseRequest
symbolsignaturewhat it does
StreamParserobject (state, parsed head fields, headers*, errorStatus*…)Resumable HTTP/1.x parser instance.
StreamKindenum skRequest, skResponseWhich grammar the first line follows.
StreamStateenum ssLine, ssHeaders, ssBody, ssChunkSize, …, ssComplete, ssErrorWhere the parser currently sits.
newRequestParserproc(): StreamParserParser expecting a request (method/target/version first).
newResponseParserproc(): StreamParserParser expecting a response (version/status/reason first).
withLimitsproc(p: var StreamParser; maxLine, maxHeaderBytes: int)Override the default 8 KiB line / 64 KiB header-block limits.
feedproc(p: var StreamParser; data: string): intFeed the next bytes; returns bytes consumed (never over-reads past a complete message).
finishproc(p: var StreamParser)Signal end-of-input (connection close); completes a read-until-close body, else records truncation.
isCompleteproc(p: StreamParser): boolThe whole message has been parsed.
isErrorproc(p: StreamParser): boolAn unrecoverable framing/limit error occurred (see errorStatus).
needMoreproc(p: StreamParser): boolHealthy but waiting for more bytes.
takeBodyproc(p: var StreamParser): stringPull and clear decoded body bytes accumulated so far.
bodyLengthproc(p: StreamParser): intTotal decoded body bytes seen (including already-taken).
headerValueproc(p: StreamParser; name: string): stringFirst matching header value from the parsed head.
toRequestproc(p: StreamParser): RequestSnapshot the parsed request head + un-taken body as a Request.
toResponseproc(p: StreamParser): ResponseSnapshot the parsed response head + un-taken body as a Response.

Status codes

symbolsignaturewhat it does
reasonPhraseproc(status: int): stringStandard reason for a code; "" for unknown (never a misleading "OK").
codeproc(n: int): HttpCodeWrap an int as HttpCode.
toIntproc(c: HttpCode): intUnderlying integer.
`==`proc(a, b: HttpCode): boolValue equality.
is1xxis5xxproc(c: HttpCode): boolClass predicates (is1xx, is2xx, is3xx, is4xx, is5xx).
`$`proc(c: HttpCode): string"200 OK", or just the number when the code has no known phrase.

Response building

symbolsignaturewhat it does
responseproc(status: int; contentType, body: string): ResponseBuild the in-memory response model.
withHeaderproc(res: var Response; name, value: string)Append a header.
responseToStringproc(res: Response; includeBody = true): stringSerialize a full HTTP/1.1 response; auto-adds Content-Type, Content-Length, Connection: close unless already supplied.
httpResponseproc(status: int; contentType, body: string): stringOne-shot response builder.
httpResponseproc(status: int; contentType, body: string; headers: seq[Header]): stringOne-shot with extra headers.
redirectproc(location: string; status = 302): stringResponse with a Location header.
optionsResponseproc(allowed: string): string204 with an Allow header (preflight/OPTIONS).

Chunked transfer

symbolsignaturewhat it does
encodeChunkedproc(body: string): stringEncode as one chunk plus the zero-length terminator.
decodeChunkedproc(s: string): stringDecode a chunked payload back to the raw body; chunk extensions handled, trailers ignored.

URL, query & form codecs

symbolsignaturewhat it does
pathOnlyproc(target: string): stringRequest-target without ?query or #fragment.
queryStringproc(target: string): stringQuery portion without the leading ?.
percentDecodeproc(s: string; plusAsSpace = false): stringRFC 3986 percent-decode; invalid %xx copied verbatim.
percentEncodeproc(s: string; plusForSpace = false): stringPercent-encode every non-unreserved byte; optional + for space.
queryParamproc(target, key: string): stringFirst decoded value for key in the target's query, or "".
formParamproc(body, key: string): stringSame lookup over an application/x-www-form-urlencoded body.
queryParamsproc(q: string): seq[(string, string)]All decoded key/value pairs in order (duplicates preserved).
encodeQueryproc(pairs: openArray[(string, string)]): stringBuild a k=v&k=v string, form-urlencoded (+ for space).

Content-Encoding — import http/contentcoding

Opt-in submodule; re-exports the compress package and adds HTTP negotiation policy. Not pulled in by import http.

symbolsignaturewhat it does
pickEncodingproc(acceptEncoding: string): stringBest supported coding for an Accept-Encoding header: prefers br, then zstd, then gzip, else "" (identity).
encodeForproc(encoding, body: string): stringEncode a body for a chosen Content-Encoding; unchanged for identity/unknown.
decodeFromproc(encoding, body: string): stringDecode a body received with the given Content-Encoding; unchanged for identity/unknown.

Design notes

  • Transport-free by design. No socket loop, no filesystem, no aoughwl substrate — just stringstring transformations. This is what lets one HTTP layer back serve, a client, or a test harness unchanged.
  • Nothing raises. parseRequest is deliberately tolerant: a short or malformed message produces empty fields (check isValidRequest), and reasonPhrase returns "" rather than inventing a phrase for an unknown code.
  • Caller-owned buffers, no slices. nimony string slicing raises on out-of-range, so every parser/codec char-walks the input with explicit bounds and copies into fresh result strings — no aliasing, no hidden allocs on the caller's buffer.
  • Case-insensitive headers, preserved spelling. Lookups fold ASCII case, but the original name is kept for emission and diagnostics.
  • Consolidates the stdlib split. Covers ground Nim 2 spreads across std/httpcore and std/uri behind one import.

Requirements

  • nimony toolchain (Nim 3.0-class). The core (http and its submodules) is standard-library only — no C FFI, no external repos.
  • http/contentcoding additionally depends on the aoughwl compress package (gzip / brotli / zstd codecs), which it re-exports.

Parsing a response

parseResponse is the client-side mirror of parseRequest: status line, headers, body, with headerValue / hasHeader overloads to match. Two decisions are worth knowing:

  • Transfer framing is not applied. A chunked body comes back chunked and decodeChunked is the caller's next step — which is what lets a streaming client act on headers before the body has arrived.
  • Malformed input reports rather than raises, like parseRequest: a response that never parsed has status == 0.

parseStatusLine and responseHeaderEnd are exported alongside, because an incremental client has to know the header block has fully arrived before it can work out how much body it is owed. serve's async client is built on exactly these three.

aoughwl — self-hosted platform for things n stuff. Contact / Support on Discord for access to the private backends.