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 DefaultCompressLevel: int64zlib's default, and the knee of the curve: 7-9 cost several times the CPU for a few percent.
const DefaultCompressMinLen: int64The smallest body worth compressing. Below about a kilobyte the gzip framing and the code tables eat most of what is saved, and the body fits in the same packet either way.
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 compression: bool compressLevel: int64 compressMinLen: int64
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 compression: bool compressLevel: int64 compressMinLen: int64 coding: ContentCoding codedStream: bool enc: Encoder zbuf: string
proc listenHttp(port: uint16; tags: ref HttpTags.Obj not nil; backlog: int64): HttpServerListen on
port.initIoRing()must already have run.backlogis how many completed handshakes the kernel may queue foracceptbefore it starts dropping new ones, and the kernel clamps it tosomaxconn. It is a burst buffer, not a connection limit โ the connections already accepted do not occupy it โ so it should be sized for the fastest rate the peer can connect at, not for steady state. 128 is what this used to be, and it is not enough: a client opening 8192 connections at once overflowed it and lost 19% of them to dropped SYNs, while the same run at 4096 lost none. (nginx and Node both default to 511; 1024 is the same idea with a little more room.)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 takeStreams(c: var HttpConnection): tuple[Reader, Writer]The connection's streams, given up.
An upgrade โ to WebSocket, say โ is exactly this: from here on the bytes on the descriptor are not HTTP, and the HTTP layer must not touch them. The caller owns the streams from now on, and the connection is closed so that a
nextafter the upgrade cannot read a second request out of a stream that is no longer HTTP.The bytes the HTTP layer had already read travel with the
Readerin its own buffer: a client that sent its first frame right behind the handshake request has them already read, and they are the new protocol's, not the old one's.proc requestHeader(c: HttpConnection; name: string): stringThe value of the request's header
name, whatever its spelling;""when the request does not carry it.The name is matched case-insensitively, and the headers nobody registered are searched too: a protocol that brings its own headers has no tag for them, and the handler is the one that knows their names.
proc countRequestHeader(c: HttpConnection; name: string): int64How many times the request carries the header
name.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 version(c: HttpConnection): TagIdThe HTTP version of the request.
tag(tV11)for HTTP/1.1 โ the only version a WebSocket handshake may use, since RFC 6455 ยง4.2.1 says so.proc 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[char]): 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 readContent(c: var HttpConnection; limit: int64): stringThe whole body with its
Content-Encodingundone: what a client that compressed its upload meant to send.limitcounts decoded bytes, which are the ones that cost memory.UnimplementedOperationfor a coding this server cannot undo โ the handler's cue to answer 415 โ andSyntaxErrorfor one that is corrupt or cut short, plus whatreadBodyraises.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[char])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.A handler that set
Content-EncodingorContent-Lengthitself has taken the body's encoding into its own hands, and it is left alone.proc respond(c: var HttpConnection; status: int64; body: openArray[char]; 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[char])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.Under compression each
writeis flushed through the encoder, so what was written is decodable by the peer when this returns โ the same promise as without it, paid for with a few bytes per call. Write in pieces the peer is meant to see, not a byte at a time.proc write(c: var HttpConnection; data: string)proc finish(c: var HttpConnection)End a streamed body.