httpserver
nimony/lib/std/httpserver.nim
const DefaultHeadMs: int64How long a peer may take to finish sending a request head. Short on purpose and separate from the request budget: a head arriving one byte per second is the cheapest denial of service there is, and it costs the attacker nothing precisely because it never looks like a timeout of anything else.
const DefaultRequestMs: int64The budget for one whole request-response exchange once its head has arrived, body and response included.
const DefaultIdleMs: int64How long a kept-alive connection may sit between requests.
const DefaultMaxRequests: int64Requests served on one connection before it is closed anyway. Not a resource limit — it is what keeps a fleet's connections rotating, so a deploy or a DNS change is picked up without waiting for peers to volunteer.
const MaxDrain: int64The most unread request body that is worth reading and discarding to keep a connection alive. Past it the connection is closed instead: draining a body the handler did not want is work a peer chose for us, and a limit is the difference between politeness and being driven.
type HttpServer = object fd: int32 tags: ref HttpTags.Obj not nil headMs: int64 requestMs: int64 idleMs: int64 maxRequests: int64 serverName: string checkTargets: bool
func dollar`.Phase(e: Phase): stringtype HttpConnection = object conn: HttpConn peer: PeerAddr req: HttpMsg res: HttpMsg pathBuf: string queryBuf: string targetBuf: string phase: Phase keepAlive: bool served: int64 bodyLeft: int64 chunked: bool headMs: int64 requestMs: int64 idleMs: int64 maxRequests: int64 serverName: string checkTargets: bool
proc listenHttp(port: uint16; tags: ref HttpTags.Obj not nil; backlog: int64): HttpServerListen on
port.initIoRing()must already have run.tagsis the process's one tag space (newHttpTags(), once, during init) — every message on every connection is built and parsed against it, so it is threaded in here rather than looked up per request.proc close(s: var HttpServer)Stop accepting. Connections already handed out are unaffected — they own their own descriptors and finish on their own chains, which is what makes a drain-then-exit shutdown just "stop calling
accept".proc accept(s: var HttpServer): HttpConnectionThe next connection.
Answers a connection whose
isClosedis true when the listener is gone, which is whatclosedoes to a parkedacceptand is therefore how an accept loop is told to stop:while true: let c = s.accept() if c.isClosed: break submit(delay(handle(c)), -1)
Not
.raises, and not because failing is impossible: a.raisesproc cannot currently return a move-only object, and this one owns a descriptor. It reads better this way regardless — a listener stopping is how an accept loop is supposed to end, so it is a value and not an exception.No deadline either: a listener with nothing connecting to it is idle, not stuck, and the thing that ends this wait is
close.proc isClosed(c: HttpConnection): boolproc close(c: var HttpConnection)End the connection now. Idempotent.
proc next(c: var HttpConnection): boolRead the next request on this connection.
falseonce there will not be one — the peer finished, the connection was closed, or something went wrong that the peer has already been told about.Never raises. Everything that can go wrong at this level has one correct answer on the wire and this sends it: 400 for a head that does not parse or a 1.1 request with no
Host, 431 for one that is too big, 408 for a peer that stopped mid-head. A handler that had to catch those would be re-deciding, per server, a question the protocol already settled.template request(c: HttpConnection): lent HttpMsgThe parsed head, for anything the named accessors below do not cover.
template target(c: HttpConnection): lent stringExactly what the request line said, undecoded. A proxy forwards this; an origin server wants
path.template path(c: HttpConnection): lent stringThe target's path, percent-decoded and normalized, and guaranteed not to climb above
/. Safe to join to a document root.template query(c: HttpConnection): lent stringThe raw query string, still encoded —
uri.decodeQuerysplits it, and it has to split before it decodes or a%26becomes a separator.proc meth(c: HttpConnection): TagIdproc isHead(c: HttpConnection): boolproc contentLength(c: HttpConnection): int64Declared body length,
-1for a chunked body,0for none.proc hasBody(c: HttpConnection): boolproc readBody(c: var HttpConnection; dest: var openArray): int64The next piece of the request body: bytes copied,
0once it has ended.One proc for both framings, because which one the peer chose is not the handler's business — it is the difference between two correct ways of saying where a body stops, and code that asks is code that can get the answer wrong. Anything left unread is drained by the next
next.proc readBody(c: var HttpConnection; limit: int64): stringThe whole body as a string, or
ContentTooLongpastlimit.The limit has no "unlimited" spelling on purpose: a body's length is chosen by the peer, and the version of this proc without a ceiling is one
Content-Length: 999999999999away from being the whole outage.proc prepare(c: var HttpConnection; m: var HttpMsg; status: int64)Start a response into
m: the status line,Date,ServerandConnection, so a handler adds only what is its own.respondtakes it from there.proc respond(c: var HttpConnection; m: var HttpMsg; body: openArray)Send
m— built byprepareand whatever headers the handler added — withbody.Content-Lengthis filled in frombodyunless the status forbids one, and the body itself is suppressed forHEADand for the statuses that cannot carry one. Those two rules are why this exists rather thansendHead+sendBody: they are invisible when they are missing and they desynchronize the next response, not this one.proc respond(c: var HttpConnection; status: int64; body: openArray; contentType: string)The whole response in one call, which is what almost every handler wants.
proc respond(c: var HttpConnection; status: int64; body: string; contentType: string)proc redirect(c: var HttpConnection; location: string; status: int64)A redirect, with the
Locationa redirect is nothing without.proc beginStream(c: var HttpConnection; m: var HttpMsg)Start a chunk-framed response whose length is not known yet. Follow with
writeand thenfinish.Chunked rather than "write until close": a response that ends by closing cannot be followed by another, so it costs the connection — and on a proxy it costs the client the ability to tell a complete response from a truncated one.
proc beginStream(c: var HttpConnection; status: int64; contentType: string)proc write(c: var HttpConnection; data: openArray)One piece of a streamed body. Suspends until it is on the wire, so a handler that produces faster than the peer consumes is slowed by the peer rather than by a queue that grows until something notices.
Nothing is written for a
HEAD, whose response is the headers aGETwould have sent and nothing after them — so a handler streams the same way for both and does not branch.proc write(c: var HttpConnection; data: string)proc finish(c: var HttpConnection)End a streamed body.