Skip to content

chaitanyaPrabhu1/wire-http-server

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

forge-http

A production-grade HTTP/1.1 server framework written from scratch on raw TCP sockets, in TypeScript — with zero runtime dependencies.

The entire HTTP protocol layer is hand-implemented: request parsing happens byte-by-byte on top of Node's net sockets, not the http module. Routing, keep-alive, chunked transfer-encoding, WebSockets, and the full middleware stack are all built from the ground up. The only Node built-ins used are the unavoidable ones — net/tls (transport), crypto (hashing/JWT/WebSocket keys), and zlib (compression).

Think Express, but you can open the hood all the way down to the wire.

import { createServer, json, requestLogger } from "forge-http";

const app = createServer();
app.use(requestLogger());
app.use(json());

app.get("/users/:id", (req, res) => {
  res.json({ id: req.params.id });
});

await app.listen(3000);

Why this project is interesting

Most "build an HTTP server" projects either (a) wrap the http module and add routes, or (b) parse one fixed request and exit. This one implements the actual protocol, handles the hard parts correctly, and packages it as a real, usable framework with the production middleware you'd expect.

Highlights of what's implemented from first principles:

  • A streaming HTTP/1.1 parser that consumes arbitrary TCP chunks, reassembles requests split across packets, and emits multiple pipelined requests from a single buffer.
  • Connection lifecycle management — persistent keep-alive connections, request pipelining (in-order responses), idle + slowloris timeouts, and a per-connection request ceiling.
  • Security-correct framing — rejects HTTP request smuggling (Transfer-Encoding + Content-Length), conflicting Content-Length headers, obsolete line folding, and oversized headers/bodies.
  • WebSockets (RFC 6455) — the opening handshake plus a complete frame codec: fragmentation, client-frame unmasking, and ping/pong/close control frames.
  • JWT (HS256/384/512) — sign and verify with constant-time comparison and algorithm pinning to defeat alg:none confusion attacks.
  • A radix-trie router — O(path-depth) route matching with params, wildcards, and nested sub-routers.

Feature matrix

Area What's included
Protocol core Byte-level HTTP/1.1 parser · keep-alive · pipelining · chunked transfer-encoding (in & out) · HTTP/1.0 fallback · TLS/HTTPS
Routing Radix-trie router · path params (:id) · wildcards (*path) · nested routers · 405 with Allow · method dispatch
Middleware Async onion-model engine · global + route-scoped · centralized error handling
Body parsing JSON · urlencoded · text · raw · multipart/form-data file uploads (from-scratch multipart parser)
Static files MIME detection · Range requests / 206 (video streaming) · ETag · Last-Modified · conditional 304 · streaming large files
Compression gzip · brotli · deflate, with Accept-Encoding negotiation (buffered + streamed paths)
Real-time WebSockets (RFC 6455) · Server-Sent Events (SSE) · pub/sub broadcast hub
Auth JWT (HS256/384/512) · HTTP Basic · API key · role guards · constant-time secret comparison
Security CORS (with preflight) · helmet-style headers (CSP/HSTS/etc.) · signed cookies · sessions
Rate limiting Sliding-window counter · token bucket · per-key with idle eviction
Validation A small zod-like schema library with coercion + path-aware error reporting
Observability Structured logging w/ request IDs · Prometheus /metrics (counter/gauge/histogram) · liveness + readiness health checks
Reliability Graceful shutdown (drain in-flight) · keep-alive/header timeouts · request-size limits · LRU cache

Architecture

                          TCP / TLS socket (node:net / node:tls)
                                       │  raw bytes
                                       ▼
   ┌─────────────────────────────────────────────────────────────────┐
   │  Connection            keep-alive · pipelining · timeouts         │
   │     │                  socket hijack (WebSocket upgrade)          │
   │     ▼                                                             │
   │  RequestParser         incremental HTTP/1.1 state machine         │
   │     │                  Content-Length · chunked · smuggling guard │
   │     ▼                                                             │
   │  Request / Response    URL+query parse · content negotiation ·    │
   │     │                  buffered + chunked-streaming serializer    │
   │     ▼                                                             │
   │  Application           ┌─ global middleware (onion compose) ──┐   │
   │     │                  │   logger · cors · auth · rate-limit … │  │
   │     │                  └──────────────┬───────────────────────┘   │
   │     ▼                                 ▼                            │
   │  Router (radix trie)   match method + path → handler chain        │
   │     │                                                             │
   │     ▼                  error handler · 404 · 405                  │
   └─────────────────────────────────────────────────────────────────┘

Project layout

src/
├── core/            the protocol layer
│   ├── parser.ts        ← byte-level HTTP/1.1 parser (the heart)
│   ├── connection.ts    ← per-socket state machine, keep-alive, pipelining
│   ├── server.ts        ← TCP/TLS listener + graceful shutdown
│   ├── request.ts       ← Request: URL/query parse, content negotiation
│   ├── response.ts      ← Response: buffered + chunked-streaming serializer
│   ├── headers.ts       ← case-insensitive multi-value header bag
│   ├── http-error.ts    ← status-carrying error type
│   ├── status.ts        ← status codes + reason phrases
│   └── mime.ts          ← MIME table + compressibility
├── router/
│   ├── trie.ts          ← radix tree for route matching
│   ├── router.ts        ← Express-style router + mounting
│   └── compose.ts       ← async onion-model middleware engine
├── middleware/      logger · body-parser · static · compression · cors ·
│                    security-headers · rate-limit · cookie · session ·
│                    auth · validate · error-handler
├── features/        websocket · sse · jwt · validation · metrics · health
├── utils/           logger · lru
├── app.ts           the Application (router + server + WS upgrade routing)
└── index.ts         public API

Quick start

npm install        # only dev deps: typescript + @types/node
npm run build      # compile to dist/
npm test           # run the unit + integration suite

Run the examples:

npm run example:rest    # REST API with JWT auth, validation, rate limiting → :3000
npm run example:chat    # WebSocket chat room (open the printed URL)        → :3001
npm run example:files   # static server w/ compression + range requests    → :3002
npm run start           # the "kitchen sink" showcase                       → :3000
npm run bench           # throughput benchmark

Usage cookbook

Routing, params, and nested routers

import { createServer, Router } from "forge-http";

const app = createServer();

const api = new Router();
api.get("/users/:id", (req, res) => res.json({ id: req.params.id }));
api.get("/files/*path", (req, res) => res.json({ path: req.params.path })); // wildcard

app.use("/api/v1", api);   // mounted under a prefix; req.baseUrl is set
await app.listen(3000);

Middleware (onion model)

app.use(async (req, res, next) => {
  const start = Date.now();
  await next();                       // descend into inner layers
  req.log.info(`took ${Date.now() - start}ms`);
});

Body parsing & file uploads

import { json, multipart } from "forge-http";

app.post("/json", json(), (req, res) => res.json(req.body));

app.post("/upload", multipart(), (req, res) => {
  const file = req.files["avatar"];   // { filename, contentType, size, data }
  res.json({ uploaded: file });
});

Validation

import { validate, v } from "forge-http";

const schema = v.object({
  email: v.string().email(),
  age: v.number().int().min(0),
  role: v.enum("user", "admin").default("user"),
});

app.post("/signup", validate({ body: schema }), (req, res) => {
  res.status(201).json(req.body);     // body is coerced + validated
});

JWT auth

import { signJwt, bearerAuth, requireAuth, requireRole } from "forge-http";

app.post("/login", (req, res) => {
  const token = signJwt({ sub: "user-1", roles: ["admin"] }, SECRET, { expiresIn: 3600 });
  res.json({ token });
});

app.use("/admin", bearerAuth({ secret: SECRET }), requireAuth(), requireRole("admin"));

WebSockets

import { WebSocketHub } from "forge-http";
const hub = new WebSocketHub();

app.ws("/ws", (ws, req) => {
  hub.add(ws);
  ws.on("message", (data) => hub.broadcast(data));   // echo to all peers
  ws.on("close", () => console.log("bye"));
});

Server-Sent Events

import { createSSE } from "forge-http";

app.get("/events", (req, res) => {
  const sse = createSSE(res, { heartbeatMs: 15_000 });
  const t = setInterval(() => sse.send({ time: Date.now() }), 1000);
  sse.onClose(() => clearInterval(t));
});

Observability

import { HttpMetrics, HealthRegistry } from "forge-http";

const metrics = new HttpMetrics();
app.use(metrics.middleware());
app.get("/metrics", metrics.handler());           // Prometheus exposition

const health = new HealthRegistry().register("db", () => checkDb());
app.get("/healthz", health.liveness());
app.get("/readyz", health.readiness());

TLS / HTTPS

import { readFileSync } from "node:fs";
const app = createServer({
  tls: { cert: readFileSync("cert.pem"), key: readFileSync("key.pem") },
});

Graceful shutdown

app.enableGracefulShutdown();   // SIGINT/SIGTERM → stop accepting, drain, exit

Correctness & security notes

The parser is deliberately strict, because lenient HTTP parsers are where smuggling bugs live:

  • Request smugglingTransfer-Encoding together with Content-Length is rejected (400); the only supported terminal transfer-coding is chunked.
  • Conflicting Content-Length values are rejected.
  • Obsolete line folding (RFC 7230 §3.2.4) is rejected.
  • Resource limits — header block (431) and body (413) sizes are bounded; slow header delivery trips a slowloris timeout (408).
  • Constant-time comparisons for cookie signatures, JWT signatures, API keys, and Basic credentials.
  • JWT algorithm pinning rejects alg:none and unexpected algorithms.
  • Path-traversal protection in the static file server (resolved paths must stay within the root).

Tests

41 tests across unit and end-to-end suites:

  • parser.test.ts — request parsing, chunked decoding, pipelining, split packets, smuggling/limit rejections.
  • router.test.ts — static/param/wildcard matching, priority, 405 detection.
  • jwt.test.ts — sign/verify round-trip, tampering, expiry, alg:none, issuer/audience.
  • validation.test.ts — coercion, nested schemas, error paths.
  • integration.test.ts — boots a real server and drives it over HTTP: JSON, params, query arrays, 404/405, compression, CORS preflight, rate limiting, HEAD, keep-alive reuse.
npm test
# ℹ tests 41   ℹ pass 41   ℹ fail 0

Benchmark

A quick local run (npm run bench, 20k requests @ 50 concurrent, keep-alive) on a developer laptop:

throughput    ~10,000 req/s
latency p50   ~4.5 ms
latency p90   ~5.7 ms
latency p99   ~8.0 ms

Not tuned for raw speed (the focus is correctness and feature completeness), but comfortably in a reasonable range for a hand-rolled stack.


Requirements

  • Node.js ≥ 20 (developed on Node 24)
  • TypeScript 5+ (dev dependency only)

License

MIT

About

An HTTP/1.1 server framework written from scratch on raw TCP sockets in TypeScript. No http module, zero dependencies.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors