From 142e4358dfcb8d5576e3234fdb44165a81833e10 Mon Sep 17 00:00:00 2001
From: "Nadhi-(Kushi)"
Date: Sat, 28 Mar 2026 10:24:47 +0530
Subject: [PATCH 01/10] chore: fixing dynamic
---
opt/runtime.js | 35 +++--
rust-native/build.rs | 3 +
rust-native/src/lib.rs | 320 +++++++++++++++++++++++---------------
rust-native/src/router.rs | 34 +++-
scripts/build-native.mjs | 3 +
src/bridge.js | 48 ++++--
src/index.d.ts | 5 +-
7 files changed, 297 insertions(+), 151 deletions(-)
diff --git a/opt/runtime.js b/opt/runtime.js
index 9246da1..1f64d0b 100644
--- a/opt/runtime.js
+++ b/opt/runtime.js
@@ -11,26 +11,36 @@ export function createRuntimeOptimizer(routes, middlewares, options = {}) {
return {
recordDispatch(route, _request, snapshot) {
const entry = routesByHandlerId.get(route.handlerId);
- if (!entry) {
+ if (!entry || entry.settled) {
return;
}
entry.hits += 1;
- entry.lastHitAt = new Date().toISOString();
entry.bridgeObserved = true;
- if (entry.stage === "cold" && entry.hits >= HOT_HIT_THRESHOLD) {
- entry.stage = "hot";
- maybeNotify(
- notify,
- entry,
- entry.staticFastPath
- ? `${entry.label} is serving from the static fast path`
- : `${entry.label} is hot on bridge dispatch`,
- );
+ if (entry.stage === "cold") {
+ if (entry.hits >= HOT_HIT_THRESHOLD) {
+ entry.stage = "hot";
+ entry.lastHitAt = Date.now();
+ maybeNotify(
+ notify,
+ entry,
+ entry.staticFastPath
+ ? `${entry.label} is serving from the static fast path`
+ : `${entry.label} is hot on bridge dispatch`,
+ );
+
+ // Non-cache candidates: no more recording needed
+ if (!entry.cacheCandidate) {
+ entry.settled = true;
+ }
+ }
+ return;
}
+ // Only cache candidates reach here in "hot" stage
if (!entry.cacheCandidate) {
+ entry.settled = true;
return;
}
@@ -47,6 +57,8 @@ export function createRuntimeOptimizer(routes, middlewares, options = {}) {
entry.stableResponses >= STABLE_RESPONSE_THRESHOLD
) {
entry.recommendation = "cache-candidate";
+ entry.settled = true;
+ entry.lastHitAt = Date.now();
maybeNotify(
notify,
entry,
@@ -156,6 +168,7 @@ function buildRouteEntry(route, middlewares) {
reasons,
stableResponses: 0,
lastResponseKey: null,
+ settled: false,
};
}
diff --git a/rust-native/build.rs b/rust-native/build.rs
index 0f1b010..45f48ad 100644
--- a/rust-native/build.rs
+++ b/rust-native/build.rs
@@ -1,3 +1,6 @@
+// Napi rs build stuff
+// OMG why do we even have this yuh.
+
fn main() {
napi_build::setup();
}
diff --git a/rust-native/src/lib.rs b/rust-native/src/lib.rs
index f9c0e58..0e62386 100644
--- a/rust-native/src/lib.rs
+++ b/rust-native/src/lib.rs
@@ -34,8 +34,9 @@ const FALLBACK_HEADER_TRANSFER_ENCODING_PREFIX: &str = "transfer-encoding:";
const BRIDGE_VERSION: u8 = 1;
const REQUEST_FLAG_QUERY_PRESENT: u16 = 1 << 0;
const REQUEST_FLAG_BODY_PRESENT: u16 = 1 << 1;
-const NOT_FOUND_HANDLER_ID: u32 = 0;
-const NOT_FOUND_BODY: &[u8] = br#"{"error":"Route not found"}"#;
+// Pre-built 404 responses — zero allocation per request
+const NOT_FOUND_RESPONSE_KEEP_ALIVE: &[u8] = b"HTTP/1.1 404 Not Found\r\ncontent-length: 27\r\nconnection: keep-alive\r\ncontent-type: application/json; charset=utf-8\r\n\r\n{\"error\":\"Route not found\"}";
+const NOT_FOUND_RESPONSE_CLOSE: &[u8] = b"HTTP/1.1 404 Not Found\r\ncontent-length: 27\r\nconnection: close\r\ncontent-type: application/json; charset=utf-8\r\n\r\n{\"error\":\"Route not found\"}";
/// Security: Maximum number of headers we allow per request
const MAX_HEADER_COUNT: usize = 64;
@@ -522,22 +523,11 @@ async fn handle_connection_inner(
let has_body = parsed.has_body;
let content_length = parsed.content_length;
- // Extract owned copies from parsed (which borrows buffer) before we mutate buffer
- let method_owned: Vec = parsed.method.to_vec();
- let target_owned: Vec = parsed.target.to_vec();
- let path_owned: Vec = parsed.path.to_vec();
- let headers_owned: Vec<(String, String)> = parsed
- .headers
- .iter()
- .map(|(n, v)| (n.to_string(), v.to_string()))
- .collect();
-
- drop(parsed);
-
- // ── Fast path: static routes (GET /) ──
- if !has_body && method_owned == b"GET" {
- if path_owned == b"/" {
+ // ── Fast path: static routes (zero-copy from borrowed parse data) ──
+ if !has_body && parsed.method == b"GET" {
+ if parsed.path == b"/" {
if let Some(static_route) = router.exact_get_root() {
+ drop(parsed);
drain_consumed_bytes(buffer, header_bytes);
write_exact_static_response(stream, static_route, keep_alive).await?;
if !keep_alive {
@@ -547,7 +537,8 @@ async fn handle_connection_inner(
continue;
}
}
- if let Some(static_route) = router.exact_static_route(&method_owned, &path_owned) {
+ if let Some(static_route) = router.exact_static_route(parsed.method, parsed.path) {
+ drop(parsed);
drain_consumed_bytes(buffer, header_bytes);
write_exact_static_response(stream, static_route, keep_alive).await?;
if !keep_alive {
@@ -558,12 +549,48 @@ async fn handle_connection_inner(
}
}
- // ── Read request body if present ──────────────────────────────
- let body_bytes: Vec = if has_body {
+ // ── Zero-copy path: non-body requests ──
+ // Build dispatch envelope directly from borrowed parse data, avoiding
+ // String/Vec allocations for method, target, path, and headers.
+ if !has_body {
+ let dispatch_request =
+ build_dispatch_request_zero_copy(router, &parsed, &[])?;
+ drop(parsed);
+ drain_consumed_bytes(buffer, header_bytes);
+
+ match dispatch_request {
+ Some(request) => {
+ write_dynamic_dispatch_response(stream, dispatcher, request, keep_alive)
+ .await?;
+ }
+ None => {
+ write_not_found_response(stream, keep_alive).await?;
+ }
+ }
+
+ if !keep_alive {
+ stream.shutdown().await?;
+ return Ok(());
+ }
+ continue;
+ }
+
+ // ── Body requests: need owned copies to release buffer for body read ──
+ let method_owned: Vec = parsed.method.to_vec();
+ let target_owned: Vec = parsed.target.to_vec();
+ let path_owned: Vec = parsed.path.to_vec();
+ let headers_owned: Vec<(String, String)> = parsed
+ .headers
+ .iter()
+ .map(|(n, v)| (n.to_string(), v.to_string()))
+ .collect();
+ drop(parsed);
+
+ // ── Read request body ──────────────────────────────────────
+ let body_bytes: Vec = {
let content_length = match content_length {
Some(len) => len,
None => {
- // Chunked or unknown body length — reject for now
let response =
build_error_response_bytes(411, b"{\"error\":\"Length Required\"}", false);
let (write_result, _) = stream.write_all(response).await;
@@ -573,7 +600,6 @@ async fn handle_connection_inner(
}
};
- // Security: enforce max body size
if content_length > MAX_BODY_BYTES {
let response =
build_error_response_bytes(413, b"{\"error\":\"Payload Too Large\"}", false);
@@ -583,7 +609,6 @@ async fn handle_connection_inner(
return Ok(());
}
- // Some body bytes may already be in the buffer after the headers
let already_in_buffer = if buffer.len() > header_bytes {
buffer.len() - header_bytes
} else {
@@ -591,12 +616,10 @@ async fn handle_connection_inner(
};
if already_in_buffer >= content_length {
- // Entire body is already in the buffer
let body = buffer[header_bytes..header_bytes + content_length].to_vec();
drain_consumed_bytes(buffer, header_bytes + content_length);
body
} else {
- // Need to read more bytes from the stream
let mut body = Vec::with_capacity(content_length);
if already_in_buffer > 0 {
body.extend_from_slice(&buffer[header_bytes..]);
@@ -609,19 +632,15 @@ async fn handle_connection_inner(
let (read_result, returned_buf) = stream.read(chunk_buf).await;
let bytes_read = read_result?;
if bytes_read == 0 {
- return Ok(()); // Connection closed mid-body
+ return Ok(());
}
body.extend_from_slice(&returned_buf[..bytes_read]);
}
body.truncate(content_length);
body
}
- } else {
- drain_consumed_bytes(buffer, header_bytes);
- Vec::new()
};
- // Dynamic path: build bridge envelope and dispatch to JS
let dispatch_request = build_dispatch_request_owned(
router,
&method_owned,
@@ -822,6 +841,47 @@ fn parse_hot_root_request(
//
// Uses the pre-parsed headers from httparse — no second scan of the raw bytes.
+/// Zero-copy dispatch: builds the bridge envelope directly from borrowed parse data,
+/// avoiding all String/Vec allocations for method, target, path, and headers.
+/// Used for non-body requests (GET, DELETE without body, etc.).
+fn build_dispatch_request_zero_copy(
+ router: &Router,
+ parsed: &ParsedRequest<'_>,
+ body: &[u8],
+) -> Result
+| http-native | static | 95,366.30 | 2.09 | 61.65 | 14.55 |
+| http-native | dynamic | 46,880.14 | 4.26 | 23.99 | 7.19 |
+| http-native | opt | 68,364.14 | 2.92 | 19.77 | 11.86 |
+| bun | static | 50,728.86 | 3.94 | 10.72 | 7.93 |
+| bun | dynamic | 46,020.19 | 4.34 | 10.80 | 7.24 |
+| bun | opt | 50,000.29 | 4.00 | 12.41 | 8.87 |
+| fiber | static | 91,129.18 | 2.19 | 25.62 | 13.22 |
+| fiber | dynamic | 89,940.82 | 2.22 | 22.58 | 13.12 |
+| fiber | opt | 88,203.77 | 2.26 | 23.64 | 14.63 |
+
Http-native
Http native is a express like server framework for Javascript that uses the Node-compatible framework with Rust native module way, where the rust binary is evoked through napi-rs or something faster.
diff --git a/src/native.js b/src/native.js
index ac605b8..d205b40 100644
--- a/src/native.js
+++ b/src/native.js
@@ -7,7 +7,7 @@ const require = createRequire(import.meta.url);
const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
export function loadNativeModule() {
- const configuredPath = process.env.HTTP_NATIVE_NODE_PATH;
+ const configuredPath = process.env.HTTP_NATIVE_NATIVE_PATH ?? process.env.HTTP_NATIVE_NODE_PATH;
const nativeModulePath = configuredPath
? resolve(rootDir, configuredPath)
: resolve(rootDir, "http-native.node");
From 301d317acbd74a0dddad2f3568cf3fc84992df9a Mon Sep 17 00:00:00 2001
From: "Nadhi-(Kushi)"
Date: Sat, 28 Mar 2026 12:37:31 +0530
Subject: [PATCH 08/10] chore: fix CI
---
.github/workflows/main.yml | 38 ++++++++++++++++++++++++++------------
bench/ci.js | 4 ++--
2 files changed, 28 insertions(+), 14 deletions(-)
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 0e4acff..182d250 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -10,9 +10,9 @@ on:
workflow_dispatch:
inputs:
engines:
- description: 'Comma-separated engines to benchmark. Supported by this workflow: "http-native,bun,fiber"'
+ description: 'Comma-separated engines to benchmark. Supported by this workflow default run: "http-native,bun"'
required: false
- default: "http-native,bun,fiber"
+ default: "http-native,bun"
scenarios:
description: 'Comma-separated scenarios: "static,dynamic,opt"'
required: false
@@ -29,11 +29,6 @@ on:
description: "Bombardier timeout"
required: false
default: "2s"
- http_native_runtime:
- description: 'Runtime for http-native target: "bun" or "node"'
- required: false
- default: "bun"
-
jobs:
benchmark:
runs-on: ubuntu-latest
@@ -50,6 +45,11 @@ jobs:
with:
bun-version: latest
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: "20"
+
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
@@ -63,15 +63,27 @@ jobs:
go install github.com/codesenberg/bombardier@latest
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
- - name: Run benchmarks
+ - name: Run benchmarks (http-native on Bun + selected engines)
+ run: |
+ bun bench/ci.js \
+ --engines="${{ github.event.inputs.engines || 'http-native,bun' }}" \
+ --scenarios="${{ github.event.inputs.scenarios || 'static,dynamic,opt' }}" \
+ --connections="${{ github.event.inputs.connections || '200' }}" \
+ --duration="${{ github.event.inputs.duration || '10s' }}" \
+ --timeout="${{ github.event.inputs.timeout || '2s' }}" \
+ --http-native-runtime="bun" \
+ --output-dir="bench/results/bun"
+
+ - name: Run benchmarks (http-native on Node)
run: |
bun bench/ci.js \
- --engines="${{ github.event.inputs.engines || 'http-native,bun,fiber' }}" \
+ --engines="http-native" \
--scenarios="${{ github.event.inputs.scenarios || 'static,dynamic,opt' }}" \
--connections="${{ github.event.inputs.connections || '200' }}" \
--duration="${{ github.event.inputs.duration || '10s' }}" \
--timeout="${{ github.event.inputs.timeout || '2s' }}" \
- --http-native-runtime="${{ github.event.inputs.http_native_runtime || 'bun' }}"
+ --http-native-runtime="node" \
+ --output-dir="bench/results/node"
- name: Upload benchmark artifacts
if: always()
@@ -79,6 +91,8 @@ jobs:
with:
name: benchmark-results
path: |
- bench/results/results.json
- bench/results/summary.md
+ bench/results/bun/results.json
+ bench/results/bun/summary.md
+ bench/results/node/results.json
+ bench/results/node/summary.md
if-no-files-found: ignore
diff --git a/bench/ci.js b/bench/ci.js
index 600093b..c9c07ff 100644
--- a/bench/ci.js
+++ b/bench/ci.js
@@ -5,7 +5,7 @@ import { once } from "node:events";
import { resolve } from "node:path";
import { homedir } from "node:os";
-const DEFAULT_ENGINES = ["http-native", "bun", "fiber"];
+const DEFAULT_ENGINES = ["http-native", "bun"];
const DEFAULT_SCENARIOS = ["static", "dynamic", "opt"];
const DEFAULT_CONNECTIONS = 200;
const DEFAULT_DURATION = "10s";
@@ -158,7 +158,7 @@ function printUsage() {
console.log("Usage: bun bench/ci.js [options]");
console.log("");
console.log("Options:");
- console.log(` --engines=http-native,bun,fiber Comma-separated list. Default: ${DEFAULT_ENGINES.join(",")}`);
+ console.log(` --engines=http-native,bun Comma-separated list. Default: ${DEFAULT_ENGINES.join(",")}`);
console.log(` --scenarios=static,dynamic,opt Comma-separated list. Default: ${DEFAULT_SCENARIOS.join(",")}`);
console.log(` --connections=${DEFAULT_CONNECTIONS} Bombardier concurrency`);
console.log(` --duration=${DEFAULT_DURATION} Bombardier duration`);
From 2ef3ef894c26047d708c74e8c8fe2afbed4944b7 Mon Sep 17 00:00:00 2001
From: "Nadhi-(Kushi)"
Date: Sat, 28 Mar 2026 12:53:27 +0530
Subject: [PATCH 09/10] opt: remove codec overhead
---
src/bridge.js | 77 ++++++++++++++++++++++++++-------------------------
1 file changed, 40 insertions(+), 37 deletions(-)
diff --git a/src/bridge.js b/src/bridge.js
index 8e745f9..47337d4 100644
--- a/src/bridge.js
+++ b/src/bridge.js
@@ -419,31 +419,30 @@ export function createJsonSerializer(mode = "fallback") {
export function decodeRequestEnvelope(buffer) {
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
let offset = 0;
- const version = readU8(view, offset);
+ const version = readU8(bytes, offset);
offset += 1;
if (version !== BRIDGE_VERSION) {
throw new Error(`Unsupported request envelope version ${version}`);
}
- const methodCode = readU8(view, offset);
+ const methodCode = readU8(bytes, offset);
offset += 1;
- const flags = readU16(view, offset);
+ const flags = readU16(bytes, offset);
offset += 2;
- const handlerId = readU32(view, offset);
+ const handlerId = readU32(bytes, offset);
offset += 4;
- const urlLength = readU32(view, offset);
+ const urlLength = readU32(bytes, offset);
offset += 4;
- const pathLength = readU16(view, offset);
+ const pathLength = readU16(bytes, offset);
offset += 2;
- const paramCount = readU16(view, offset);
+ const paramCount = readU16(bytes, offset);
offset += 2;
- const headerCount = readU16(view, offset);
+ const headerCount = readU16(bytes, offset);
offset += 2;
- const bodyLength = readU32(view, offset);
+ const bodyLength = readU32(bytes, offset);
offset += 4;
const urlBytes = readBytes(bytes, offset, urlLength);
@@ -451,20 +450,20 @@ export function decodeRequestEnvelope(buffer) {
const pathBytes = readBytes(bytes, offset, pathLength);
offset += pathLength;
- const paramValues = new Array(paramCount);
+ const paramValues = paramCount === 0 ? EMPTY_ARRAY : new Array(paramCount);
for (let index = 0; index < paramCount; index += 1) {
- const valueLength = readU16(view, offset);
+ const valueLength = readU16(bytes, offset);
offset += 2;
const valueBytes = readBytes(bytes, offset, valueLength);
offset += valueLength;
paramValues[index] = valueBytes;
}
- const rawHeaders = new Array(headerCount);
+ const rawHeaders = headerCount === 0 ? EMPTY_ARRAY : new Array(headerCount);
for (let index = 0; index < headerCount; index += 1) {
- const nameLength = readU8(view, offset);
+ const nameLength = readU8(bytes, offset);
offset += 1;
- const valueLength = readU16(view, offset);
+ const valueLength = readU16(bytes, offset);
offset += 2;
const nameBytes = readBytes(bytes, offset, nameLength);
offset += nameLength;
@@ -545,20 +544,19 @@ export function encodeResponseEnvelope(snapshot) {
}
const output = Buffer.allocUnsafe(totalLength);
- const view = new DataView(output.buffer, output.byteOffset, output.byteLength);
let offset = 0;
- view.setUint16(offset, Number(snapshot.status ?? 200), true);
+ writeU16(output, offset, Number(snapshot.status ?? 200));
offset += 2;
- view.setUint16(offset, headerCount, true);
+ writeU16(output, offset, headerCount);
offset += 2;
- view.setUint32(offset, body.length, true);
+ writeU32(output, offset, body.length);
offset += 4;
for (let i = 0; i < headerCount; i++) {
const [nameBytes, valueBytes] = encodedHeaders[i];
output[offset++] = nameBytes.length;
- view.setUint16(offset, valueBytes.length, true);
+ writeU16(output, offset, valueBytes.length);
offset += 2;
output.set(nameBytes, offset);
offset += nameBytes.length;
@@ -812,35 +810,40 @@ function readBytes(bytes, offset, length) {
return bytes.subarray(offset, offset + length);
}
-function readU8(view, offset) {
- if (offset + 1 > view.byteLength) {
+function readU8(bytes, offset) {
+ if (offset + 1 > bytes.byteLength) {
throw new Error("Request envelope truncated");
}
- return view.getUint8(offset);
+ return bytes[offset];
}
-function readU16(view, offset) {
- if (offset + 2 > view.byteLength) {
+function readU16(bytes, offset) {
+ if (offset + 2 > bytes.byteLength) {
throw new Error("Request envelope truncated");
}
- return view.getUint16(offset, true);
+ return bytes[offset] | (bytes[offset + 1] << 8);
}
-function readU32(view, offset) {
- if (offset + 4 > view.byteLength) {
+function readU32(bytes, offset) {
+ if (offset + 4 > bytes.byteLength) {
throw new Error("Request envelope truncated");
}
- return view.getUint32(offset, true);
+ return (
+ bytes[offset] |
+ (bytes[offset + 1] << 8) |
+ (bytes[offset + 2] << 16) |
+ (bytes[offset + 3] << 24 >>> 0)
+ ) >>> 0;
}
-function writeU8(view, offset, value) {
- view.setUint8(offset, value);
+function writeU16(bytes, offset, value) {
+ bytes[offset] = value & 0xff;
+ bytes[offset + 1] = (value >>> 8) & 0xff;
}
-function writeU16(view, offset, value) {
- view.setUint16(offset, value, true);
-}
-
-function writeU32(view, offset, value) {
- view.setUint32(offset, value, true);
+function writeU32(bytes, offset, value) {
+ bytes[offset] = value & 0xff;
+ bytes[offset + 1] = (value >>> 8) & 0xff;
+ bytes[offset + 2] = (value >>> 16) & 0xff;
+ bytes[offset + 3] = (value >>> 24) & 0xff;
}
From 935e264294cc8823d91c6f61274bea925a5a169c Mon Sep 17 00:00:00 2001
From: "Nadhi-(Kushi)"
Date: Sat, 28 Mar 2026 18:02:52 +0530
Subject: [PATCH 10/10] opt&chore: headers and etc are precomputed & fixed the
contibuting.md
---
CONTRIBUTING.md | 8 +-
examples/cors/server.js | 8 +-
examples/middleware/server.js | 10 +--
examples/rest-api/server.js | 14 ++--
examples/validation/server.js | 6 +-
readme.md | 9 ---
rust-native/src/lib.rs | 138 ++++++++++++++-----------------
rust-native/src/router.rs | 12 +--
src/bridge.js | 147 +++++++++++++++++++---------------
src/index.d.ts | 10 +--
src/index.js | 28 ++++---
test/app.js | 22 +++++
12 files changed, 217 insertions(+), 195 deletions(-)
create mode 100644 test/app.js
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 029e362..a7b0502 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,10 +2,10 @@
When commiting code please mention what type of commit it is via the prefix. Example,
-opt: This flag is used to show optimizations
-chore: Fix / chore
-rm: removal
-other: Everything else.
+opt: This flag is used to show optimizations
+chore: Fix / chore
+rm: removal
+other: Everything else.
A commit would like this,
diff --git a/examples/cors/server.js b/examples/cors/server.js
index f1a200c..c085c34 100644
--- a/examples/cors/server.js
+++ b/examples/cors/server.js
@@ -3,13 +3,13 @@ import { cors } from "http-native/cors";
const app = createApp();
-// ─── Example 1: Allow all origins ─────────────────────────────────────────────
+// ─── Example 1: Allow all origins ───────
// app.use(cors());
-// ─── Example 2: Allow specific origin ─────────────────────────────────────────
+// ─── Example 2: Allow specific origin ───
// app.use(cors({ origin: "https://myapp.com" }));
-// ─── Example 3: Allow multiple origins ────────────────────────────────────────
+// ─── Example 3: Allow multiple origins ──
// app.use(cors({ origin: ["https://myapp.com", "https://admin.myapp.com"] }));
// ─── Example 4: Dynamic origin with credentials ──────────────────────────────
@@ -29,7 +29,7 @@ app.use(
}),
);
-// ─── Routes ───────────────────────────────────────────────────────────────────
+// ─── Routes ─────────────────────────────
app.get("/api/data", (req, res) => {
res.set("X-Request-Id", crypto.randomUUID()).json({
diff --git a/examples/middleware/server.js b/examples/middleware/server.js
index a7eade9..2816167 100644
--- a/examples/middleware/server.js
+++ b/examples/middleware/server.js
@@ -2,7 +2,7 @@ import { createApp } from "http-native";
const app = createApp();
-// ─── Logging Middleware (global) ──────────────────────────────────────────────
+// ─── Logging Middleware (global) ────────
app.use(async (req, res, next) => {
const start = performance.now();
@@ -14,7 +14,7 @@ app.use(async (req, res, next) => {
console.log(`← ${req.method} ${req.path} [${duration}ms]`);
});
-// ─── Auth Middleware (scoped to /api) ─────────────────────────────────────────
+// ─── Auth Middleware (scoped to /api) ───
app.use("/api", async (req, res, next) => {
const token = req.header("authorization");
@@ -39,7 +39,7 @@ app.use("/api", async (req, res, next) => {
await next();
});
-// ─── Request ID Middleware (global) ────────────────────────────────────────────
+// ─── Request ID Middleware (global) ──────
app.use(async (req, res, next) => {
const requestId = crypto.randomUUID();
@@ -48,7 +48,7 @@ app.use(async (req, res, next) => {
await next();
});
-// ─── Routes ───────────────────────────────────────────────────────────────────
+// ─── Routes ─────────────────────────────
// Public route (only logging + request ID middlewares run)
app.get("/", (req, res) => {
@@ -70,7 +70,7 @@ app.get("/api/secret", (req, res) => {
});
});
-// ─── Error Handler ────────────────────────────────────────────────────────────
+// ─── Error Handler ──────────────────────
app.onError((err, req, res) => {
console.error(`❌ Error on ${req.method} ${req.path}:`, err.message);
diff --git a/examples/rest-api/server.js b/examples/rest-api/server.js
index 2e0a144..636d3f5 100644
--- a/examples/rest-api/server.js
+++ b/examples/rest-api/server.js
@@ -12,14 +12,14 @@ todos.set(2, { id: 2, title: "Build something awesome", completed: false });
todos.set(3, { id: 3, title: "Deploy to production", completed: false });
nextId = 4;
-// ─── Error Handler ────────────────────────────────────────────────────────────
+// ─── Error Handler ──────────────────────
app.onError((err, req, res) => {
console.error(`Error: ${err.message}`);
res.status(500).json({ error: "Internal server error" });
});
-// ─── List all todos ───────────────────────────────────────────────────────────
+// ─── List all todos ─────────────────────
app.get("/todos", (req, res) => {
const { completed } = req.query;
@@ -34,7 +34,7 @@ app.get("/todos", (req, res) => {
res.json({ todos: items, count: items.length });
});
-// ─── Get single todo ──────────────────────────────────────────────────────────
+// ─── Get single todo ────────────────────
app.get("/todos/:id", (req, res) => {
const id = Number(req.params.id);
@@ -48,7 +48,7 @@ app.get("/todos/:id", (req, res) => {
res.json(todo);
});
-// ─── Create todo ──────────────────────────────────────────────────────────────
+// ─── Create todo ────────────────────────
app.post("/todos", (req, res) => {
const body = req.json();
@@ -68,7 +68,7 @@ app.post("/todos", (req, res) => {
res.status(201).json(todo);
});
-// ─── Update todo ──────────────────────────────────────────────────────────────
+// ─── Update todo ────────────────────────
app.put("/todos/:id", (req, res) => {
const id = Number(req.params.id);
@@ -90,7 +90,7 @@ app.put("/todos/:id", (req, res) => {
res.json(updated);
});
-// ─── Delete todo ──────────────────────────────────────────────────────────────
+// ─── Delete todo ────────────────────────
app.delete("/todos/:id", (req, res) => {
const id = Number(req.params.id);
@@ -104,7 +104,7 @@ app.delete("/todos/:id", (req, res) => {
res.sendStatus(204);
});
-// ─── Toggle completion ────────────────────────────────────────────────────────
+// ─── Toggle completion ──────────────────
app.patch("/todos/:id/toggle", (req, res) => {
const id = Number(req.params.id);
diff --git a/examples/validation/server.js b/examples/validation/server.js
index a3e25dc..d75cec7 100644
--- a/examples/validation/server.js
+++ b/examples/validation/server.js
@@ -3,7 +3,7 @@ import { validate } from "http-native/validate";
const app = createApp();
-// ─── Manual Schema (no external deps) ─────────────────────────────────────────
+// ─── Manual Schema (no external deps) ───
//
// Works with any object that has .parse() or .safeParse().
// Below is a minimal hand-rolled schema — in production, use Zod:
@@ -59,14 +59,14 @@ const QuerySchema = createSchema((data) => {
return errors;
});
-// ─── Error Handler ────────────────────────────────────────────────────────────
+// ─── Error Handler ──────────────────────
app.onError((err, req, res) => {
console.error(`Error: ${err.message}`);
res.status(500).json({ error: "Internal server error" });
});
-// ─── Routes with Validation ──────────────────────────────────────────────────
+// ─── Routes with Validation ────────────
// Validates body against CreateUserSchema
app.post(
diff --git a/readme.md b/readme.md
index d812c04..d446991 100644
--- a/readme.md
+++ b/readme.md
@@ -2,15 +2,6 @@
-| http-native | static | 95,366.30 | 2.09 | 61.65 | 14.55 |
-| http-native | dynamic | 46,880.14 | 4.26 | 23.99 | 7.19 |
-| http-native | opt | 68,364.14 | 2.92 | 19.77 | 11.86 |
-| bun | static | 50,728.86 | 3.94 | 10.72 | 7.93 |
-| bun | dynamic | 46,020.19 | 4.34 | 10.80 | 7.24 |
-| bun | opt | 50,000.29 | 4.00 | 12.41 | 8.87 |
-| fiber | static | 91,129.18 | 2.19 | 25.62 | 13.22 |
-| fiber | dynamic | 89,940.82 | 2.22 | 22.58 | 13.12 |
-| fiber | opt | 88,203.77 | 2.26 | 23.64 | 14.63 |
Http-native
diff --git a/rust-native/src/lib.rs b/rust-native/src/lib.rs
index e44e4fd..69f5487 100644
--- a/rust-native/src/lib.rs
+++ b/rust-native/src/lib.rs
@@ -25,7 +25,7 @@ use crate::analyzer::{
use crate::manifest::{HttpServerConfigInput, ManifestInput};
use crate::router::{ExactStaticRoute, MatchedRoute, Router};
-// ─── Constants ────────────────────────────────────────────────────────────────
+// ─── Constants ──────────────────────────
// Gotta add support for these to be changed.
const FALLBACK_DEFAULT_HOST: &str = "127.0.0.1";
@@ -39,11 +39,9 @@ const FALLBACK_HEADER_TRANSFER_ENCODING_PREFIX: &str = "transfer-encoding:";
const BRIDGE_VERSION: u8 = 1;
const REQUEST_FLAG_QUERY_PRESENT: u16 = 1 << 0;
const REQUEST_FLAG_BODY_PRESENT: u16 = 1 << 1;
+const UNKNOWN_METHOD_CODE: u8 = 0;
/// Sentinel handler ID dispatched to JS when no route matches — JS treats this as 404.
const NOT_FOUND_HANDLER_ID: u32 = 0;
-// Pre-built 404 responses — zero allocation per request
-const NOT_FOUND_RESPONSE_KEEP_ALIVE: &[u8] = b"HTTP/1.1 404 Not Found\r\ncontent-length: 27\r\nconnection: keep-alive\r\ncontent-type: application/json; charset=utf-8\r\n\r\n{\"error\":\"Route not found\"}";
-const NOT_FOUND_RESPONSE_CLOSE: &[u8] = b"HTTP/1.1 404 Not Found\r\ncontent-length: 27\r\nconnection: close\r\ncontent-type: application/json; charset=utf-8\r\n\r\n{\"error\":\"Route not found\"}";
/// Security: Maximum number of headers we allow per request
const MAX_HEADER_COUNT: usize = 64;
@@ -63,7 +61,7 @@ const BUFFER_POOL_MAX_RECYCLE_SIZE: usize = 65536;
type DispatchTsfn = ThreadsafeFunction, Buffer, Status, false, false, 0>;
-// ─── Thread-Local Buffer Pool ─────────────────────────────────────────────────
+// ─── Thread-Local Buffer Pool ───────────
//
// Eliminates per-connection Vec allocations by recycling buffers.
@@ -92,7 +90,7 @@ fn release_buffer(mut buf: Vec) {
});
}
-// ─── Server Configuration ─────────────────────────────────────────────────────
+// ─── Server Configuration ───────────────
#[derive(Clone)]
struct HttpServerConfig {
@@ -170,7 +168,7 @@ impl HttpServerConfig {
}
}
-// ─── NAPI Interface ───────────────────────────────────────────────────────────
+// ─── NAPI Interface ─────────────────────
#[napi(object)]
pub struct NativeListenOptions {
@@ -368,7 +366,7 @@ fn worker_count_for(options: &NativeListenOptions) -> usize {
.unwrap_or(1)
}
-// ─── JS Dispatcher ────────────────────────────────────────────────────────────
+// ─── JS Dispatcher ──────────────────────
struct JsDispatcher {
callback: DispatchTsfn,
@@ -388,7 +386,7 @@ impl JsDispatcher {
}
}
-// ─── Server Loop ──────────────────────────────────────────────────────────────
+// ─── Server Loop ────────────────────────
async fn run_server(
listener: TcpListener,
@@ -437,7 +435,7 @@ async fn run_server(
Ok(())
}
-// ─── Parsed Request (from httparse) ───────────────────────────────────────────
+// ─── Parsed Request (from httparse) ─────
struct ParsedRequest<'a> {
method: &'a [u8],
@@ -451,7 +449,7 @@ struct ParsedRequest<'a> {
headers: Vec<(&'a str, &'a str)>,
}
-// ─── Connection Handler with Buffer Pool ──────────────────────────────────────
+// ─── Connection Handler with Buffer Pool
async fn handle_connection(
mut stream: TcpStream,
@@ -573,9 +571,6 @@ async fn handle_connection_inner(
let (write_result, _) = stream.write_all(response).await;
write_result?;
}
- DispatchDecision::NotFound => {
- write_not_found_response(stream, keep_alive).await?;
- }
}
if !keep_alive {
@@ -596,7 +591,7 @@ async fn handle_connection_inner(
.collect();
drop(parsed);
- // ── Read request body ──────────────────────────────────────
+ // ── Read request body
let body_bytes: Vec = {
let content_length = match content_length {
Some(len) => len,
@@ -660,14 +655,7 @@ async fn handle_connection_inner(
&body_bytes,
)?;
- match dispatch_request {
- Some(request) => {
- write_dynamic_dispatch_response(stream, dispatcher, request, keep_alive).await?;
- }
- None => {
- write_not_found_response(stream, keep_alive).await?;
- }
- }
+ write_dynamic_dispatch_response(stream, dispatcher, dispatch_request, keep_alive).await?;
if !keep_alive {
stream.shutdown().await?;
@@ -676,7 +664,7 @@ async fn handle_connection_inner(
}
}
-// ─── httparse-based Request Parsing ───────────────────────────────────────────
+// ─── httparse-based Request Parsing ─────
//
// Uses the battle-tested `httparse` crate for RFC-compliant zero-copy parsing.
// Single-pass: parses headers once and stores them for reuse by both the
@@ -769,7 +757,7 @@ fn parse_request_httparse(bytes: &[u8]) -> Option> {
})
}
-// ─── Hot Root Path (GET /) ────────────────────────────────────────────────────
+// ─── Hot Root Path (GET /) ──────────────
//
// Ultra-fast path for the most common benchmark case. Falls back to httparse
// if the request doesn't exactly match the expected prefix.
@@ -845,7 +833,7 @@ fn parse_hot_root_request(
})
}
-// ─── Routing ──────────────────────────────────────────────────────────────────
+// ─── Routing ────────────────────────────
// ─── Bridge Envelope Building (Single-Pass Headers) ───────────────────────────
//
@@ -857,7 +845,6 @@ fn parse_hot_root_request(
enum DispatchDecision {
BridgeRequest(Buffer),
SpecializedResponse(Vec),
- NotFound,
}
fn build_dispatch_decision_zero_copy(
@@ -865,25 +852,31 @@ fn build_dispatch_decision_zero_copy(
parsed: &ParsedRequest<'_>,
body: &[u8],
) -> Result {
- let Some(method_code) = method_code_from_bytes(parsed.method) else {
- return Ok(DispatchDecision::NotFound);
- };
-
- let path_str = match std::str::from_utf8(parsed.path) {
- Ok(s) => s,
- Err(_) => return Ok(DispatchDecision::NotFound),
- };
- let url_str = match std::str::from_utf8(parsed.target) {
- Ok(s) => s,
- Err(_) => return Ok(DispatchDecision::NotFound),
- };
+ let method_code = method_code_from_bytes(parsed.method).unwrap_or(UNKNOWN_METHOD_CODE);
+ let path_cow = String::from_utf8_lossy(parsed.path);
+ let path_str = path_cow.as_ref();
+ let url_cow = String::from_utf8_lossy(parsed.target);
+ let url_str = url_cow.as_ref();
let normalized_path = normalize_runtime_path(path_str);
if contains_path_traversal(&normalized_path) {
- return Ok(DispatchDecision::NotFound);
+ return build_not_found_dispatch_envelope(
+ method_code,
+ path_str,
+ url_str,
+ &parsed.headers,
+ body,
+ )
+ .map(DispatchDecision::BridgeRequest);
}
- let Some(matched_route) = router.match_route(method_code, normalized_path.as_ref()) else {
+ let matched_route = if method_code == UNKNOWN_METHOD_CODE {
+ None
+ } else {
+ router.match_route(method_code, normalized_path.as_ref())
+ };
+
+ let Some(matched_route) = matched_route else {
return build_not_found_dispatch_envelope(
method_code,
path_str,
@@ -918,40 +911,45 @@ fn build_dispatch_request_owned(
path: &[u8],
headers: &[(String, String)],
body: &[u8],
-) -> Result> {
- let Some(method_code) = method_code_from_bytes(method) else {
- return Ok(None);
- };
+) -> Result {
+ let method_code = method_code_from_bytes(method).unwrap_or(UNKNOWN_METHOD_CODE);
- let path_str = match std::str::from_utf8(path) {
- Ok(path_str) => path_str,
- Err(_) => return Ok(None),
- };
- let url_str = match std::str::from_utf8(target) {
- Ok(url_str) => url_str,
- Err(_) => return Ok(None),
- };
+ let path_cow = String::from_utf8_lossy(path);
+ let path_str = path_cow.as_ref();
+ let url_cow = String::from_utf8_lossy(target);
+ let url_str = url_cow.as_ref();
+
+ let header_refs: Vec<(&str, &str)> = headers
+ .iter()
+ .map(|(n, v)| (n.as_str(), v.as_str()))
+ .collect();
// Security: strict path validation
let normalized_path = normalize_runtime_path(path_str);
if contains_path_traversal(&normalized_path) {
- return Ok(None);
+ return build_not_found_dispatch_envelope(
+ method_code,
+ path_str,
+ url_str,
+ &header_refs,
+ body,
+ );
}
- let header_refs: Vec<(&str, &str)> = headers
- .iter()
- .map(|(n, v)| (n.as_str(), v.as_str()))
- .collect();
+ let matched_route = if method_code == UNKNOWN_METHOD_CODE {
+ None
+ } else {
+ router.match_route(method_code, normalized_path.as_ref())
+ };
- let Some(matched_route) = router.match_route(method_code, normalized_path.as_ref()) else {
+ let Some(matched_route) = matched_route else {
return build_not_found_dispatch_envelope(
method_code,
path_str,
url_str,
&header_refs,
body,
- )
- .map(Some);
+ );
};
build_dispatch_envelope(
@@ -962,7 +960,6 @@ fn build_dispatch_request_owned(
&header_refs,
body,
)
- .map(Some)
}
fn build_not_found_dispatch_envelope(
@@ -1408,7 +1405,7 @@ fn build_response_bytes_fast(
output
}
-// ─── Response Writing ─────────────────────────────────────────────────────────
+// ─── Response Writing ───────────────────
async fn write_exact_static_response(
stream: &mut TcpStream,
@@ -1539,17 +1536,6 @@ fn build_http_response_from_dispatch(dispatch_bytes: &[u8], keep_alive: bool) ->
Ok(output)
}
-async fn write_not_found_response(stream: &mut TcpStream, keep_alive: bool) -> Result<()> {
- let response = if keep_alive {
- Bytes::from_static(NOT_FOUND_RESPONSE_KEEP_ALIVE)
- } else {
- Bytes::from_static(NOT_FOUND_RESPONSE_CLOSE)
- };
- let (write_result, _) = stream.write_all(response).await;
- write_result?;
- Ok(())
-}
-
/// Build a simple error response without going through the JS bridge
fn build_error_response_bytes(status: u16, body: &[u8], keep_alive: bool) -> Vec {
build_response_bytes(
@@ -1636,7 +1622,7 @@ fn build_response_bytes(
output
}
-// ─── Security Utilities ───────────────────────────────────────────────────────
+// ─── Security Utilities ─────────────────
/// Check for path traversal attempts (../, ..\, etc.)
fn contains_path_traversal(path: &str) -> bool {
@@ -1675,7 +1661,7 @@ pub(crate) fn escape_json(value: &str) -> String {
output
}
-// ─── Helpers ──────────────────────────────────────────────────────────────────
+// ─── Helpers ────────────────────────────
fn method_code_from_bytes(method: &[u8]) -> Option {
match method {
diff --git a/rust-native/src/router.rs b/rust-native/src/router.rs
index dce4409..630636f 100644
--- a/rust-native/src/router.rs
+++ b/rust-native/src/router.rs
@@ -12,7 +12,7 @@ const ROUTE_KIND_EXACT: u8 = 1;
const ROUTE_KIND_PARAM: u8 = 2;
const MAX_STACK_SEGMENTS: usize = 16;
-// ─── Public Types ─────────────────────────────────────────────────────────────
+// ─── Public Types ───────────────────────
#[derive(Clone)]
pub struct Router {
@@ -43,7 +43,7 @@ pub struct MatchedRoute<'a, 'b> {
pub fast_path: Option<&'a DynamicFastPathSpec>,
}
-// ─── Internal Types ───────────────────────────────────────────────────────────
+// ─── Internal Types ─────────────────────
#[derive(Clone)]
struct DynamicRouteSpec {
@@ -68,7 +68,7 @@ enum MethodKey {
Put,
}
-// ─── Radix Tree ───────────────────────────────────────────────────────────────
+// ─── Radix Tree ─────────────────────────
//
// Each node represents either a static prefix or a parameter capture.
// Matching is O(M) where M is the number of path segments, not O(N) routes.
@@ -180,7 +180,7 @@ impl RadixNode {
}
}
-// ─── Router Implementation ────────────────────────────────────────────────────
+// ─── Router Implementation ──────────────
impl Router {
pub fn from_manifest(manifest: &ManifestInput) -> Result {
@@ -326,7 +326,7 @@ impl Router {
}
}
-// ─── MethodKey ────────────────────────────────────────────────────────────────
+// ─── MethodKey ──────────────────────────
impl MethodKey {
fn from_method_str(method: &str) -> Option {
@@ -369,7 +369,7 @@ impl MethodKey {
}
}
-// ─── Helpers ──────────────────────────────────────────────────────────────────
+// ─── Helpers ────────────────────────────
fn compile_dynamic_route_spec(route: &RouteInput, middlewares: &[MiddlewareInput]) -> DynamicRouteSpec {
let param_names = route
diff --git a/src/bridge.js b/src/bridge.js
index 47337d4..7cf0217 100644
--- a/src/bridge.js
+++ b/src/bridge.js
@@ -4,6 +4,8 @@ const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
const PLAIN_OBJECT_PROTOTYPE = Object.prototype;
const EMPTY_ARRAY = Object.freeze([]);
+const HEADER_LOOKUP_CACHE_MAX = 128;
+const headerLookupNameCache = new Map();
export const BRIDGE_VERSION = 1;
export const REQUEST_FLAG_QUERY_PRESENT = 1 << 0;
@@ -27,7 +29,7 @@ export const ROUTE_KIND = Object.freeze({
// Security: use null-prototype objects for user-facing data to prevent prototype pollution
const EMPTY_OBJECT = Object.freeze(Object.create(null));
-// ─── Regex patterns for source analysis ───────────────────────────────────────
+// ─── Regex patterns for source analysis ─
const PARAM_DOT_RE = /\breq\.params\.([A-Za-z_$][\w$]*)\b/g;
const PARAM_BRACKET_RE = /\breq\.params\[(['"])([^"'\\]+)\1\]/g;
@@ -59,7 +61,7 @@ const DANGEROUS_KEYS = new Set([
"__lookupSetter__",
]);
-// ─── Route Compilation ────────────────────────────────────────────────────────
+// ─── Route Compilation ──────────────────
export function compileRouteShape(method, path) {
const methodCode = METHOD_CODES[method];
@@ -90,7 +92,7 @@ export function compileRouteShape(method, path) {
};
}
-// ─── Request Access Analysis ──────────────────────────────────────────────────
+// ─── Request Access Analysis ────────────
export function analyzeRequestAccess(source) {
const plan = createEmptyAccessPlan();
@@ -203,6 +205,7 @@ export function releaseRequestObject(req) {
req._params = undefined;
req._query = undefined;
req._headers = undefined;
+ req._headerLookup = undefined;
req._decoded = null;
req._routeParamNames = null;
req._plan = null;
@@ -219,6 +222,7 @@ function createPooledRequest() {
req._params = undefined;
req._query = undefined;
req._headers = undefined;
+ req._headerLookup = undefined;
req._bodyParsed = undefined;
req._decoded = null;
req._routeParamNames = null;
@@ -288,26 +292,30 @@ function createPooledRequest() {
get() {
if (req._headers === undefined) {
const needsHeaders = req._plan.fullHeaders || req._plan.headerKeys.size > 0;
- req._headers = needsHeaders
- ? materializeHeaderObject(req._decoded.rawHeaders, req._plan)
- : EMPTY_OBJECT;
+ if (!needsHeaders) {
+ req._headers = EMPTY_OBJECT;
+ } else if (req._plan.fullHeaders) {
+ req._headers = ensureHeaderLookup(req);
+ } else {
+ req._headers = materializeSelectedHeadersFromLookup(
+ ensureHeaderLookup(req),
+ req._plan.headerKeys,
+ );
+ }
}
return req._headers;
},
});
req.header = function header(name) {
- const lookup = normalizeHeaderLookup(name);
+ const lookup = normalizeHeaderLookupCached(name);
if (req._headers && lookup in req._headers) {
return req._headers[lookup];
}
- if (req._decoded.rawHeaders.length === 0) {
- return undefined;
- }
- return lookupHeaderValue(req._decoded.rawHeaders, lookup);
+ return ensureHeaderLookup(req)[lookup];
};
- // ─── Body APIs ────────────────────────────────────────────────────────
+ // ─── Body APIs ──────────────────
Object.defineProperty(req, "body", {
configurable: true,
@@ -396,13 +404,14 @@ export function createRequestFactory(
request._params = undefined;
request._query = undefined;
request._headers = undefined;
+ request._headerLookup = undefined;
request._bodyParsed = undefined;
return request;
};
}
-// ─── JSON Serialization ───────────────────────────────────────────────────────
+// ─── JSON Serialization ─────────────────
export function createJsonSerializer(mode = "fallback") {
// Performance: V8's native JSON.stringify is heavily optimized and almost always
@@ -415,7 +424,7 @@ export function createJsonSerializer(mode = "fallback") {
return serializer;
}
-// ─── Binary Protocol Codec ────────────────────────────────────────────────────
+// ─── Binary Protocol Codec ──────────────
export function decodeRequestEnvelope(buffer) {
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
@@ -476,17 +485,19 @@ export function decodeRequestEnvelope(buffer) {
const bodyBytes = bodyLength > 0 ? readBytes(bytes, offset, bodyLength) : null;
offset += bodyLength;
- switch (methodCode) {
- case METHOD_CODES.GET:
- case METHOD_CODES.POST:
- case METHOD_CODES.PUT:
- case METHOD_CODES.DELETE:
- case METHOD_CODES.PATCH:
- case METHOD_CODES.OPTIONS:
- case METHOD_CODES.HEAD:
- break;
- default:
- throw new Error(`Unknown method code ${methodCode}`);
+ if (methodCode !== 0) {
+ switch (methodCode) {
+ case METHOD_CODES.GET:
+ case METHOD_CODES.POST:
+ case METHOD_CODES.PUT:
+ case METHOD_CODES.DELETE:
+ case METHOD_CODES.PATCH:
+ case METHOD_CODES.OPTIONS:
+ case METHOD_CODES.HEAD:
+ break;
+ default:
+ throw new Error(`Unknown method code ${methodCode}`);
+ }
}
return {
@@ -580,14 +591,6 @@ function materializeParamObject(entries, paramNames, plan) {
return materializeSelectedParamPairs(entries, paramNames, plan.paramKeys);
}
-function materializeHeaderObject(entries, plan) {
- if (plan.fullHeaders) {
- return materializePairs(entries, true);
- }
-
- return materializeSelectedPairs(entries, plan.headerKeys, true);
-}
-
function materializeQueryObject(url, flags, plan) {
if (!(flags & REQUEST_FLAG_QUERY_PRESENT)) {
return Object.create(null);
@@ -600,23 +603,6 @@ function materializeQueryObject(url, flags, plan) {
return parseSelectedQuery(url, plan.queryKeys);
}
-function materializePairs(entries, lowerCaseKeys = false) {
- // Security: null-prototype object prevents prototype pollution
- const result = Object.create(null);
-
- for (const [rawName, rawValue] of entries) {
- const name = textDecoder.decode(rawName);
- const key = lowerCaseKeys ? name.toLowerCase() : name;
- // Security: skip dangerous prototype keys
- if (DANGEROUS_KEYS.has(key)) {
- continue;
- }
- result[key] = textDecoder.decode(rawValue);
- }
-
- return result;
-}
-
function materializeParamPairs(entries, paramNames) {
const result = Object.create(null);
@@ -647,20 +633,21 @@ function materializeSelectedParamPairs(entries, paramNames, selectedKeys) {
return result;
}
-function materializeSelectedPairs(entries, selectedKeys, lowerCaseKeys = false) {
- if (selectedKeys.size === 0) {
- return Object.create(null);
+function materializeSelectedHeadersFromLookup(headerLookup, selectedKeys) {
+ if (selectedKeys.size === 0 || headerLookup === EMPTY_OBJECT) {
+ return EMPTY_OBJECT;
}
const result = Object.create(null);
- for (const [rawName, rawValue] of entries) {
- const name = textDecoder.decode(rawName);
- const key = lowerCaseKeys ? name.toLowerCase() : name;
- if (selectedKeys.has(key) && !DANGEROUS_KEYS.has(key)) {
- result[key] = textDecoder.decode(rawValue);
+ for (const key of selectedKeys) {
+ if (DANGEROUS_KEYS.has(key)) {
+ continue;
+ }
+ const value = headerLookup[key];
+ if (value !== undefined) {
+ result[key] = value;
}
}
-
return result;
}
@@ -719,18 +706,31 @@ function pushQueryEntry(result, key, value) {
result[key] = value;
}
-function lookupHeaderValue(entries, targetName) {
+function ensureHeaderLookup(req) {
+ if (req._headerLookup !== undefined) {
+ return req._headerLookup;
+ }
+
+ const entries = req._decoded.rawHeaders;
+ if (entries.length === 0) {
+ req._headerLookup = EMPTY_OBJECT;
+ return req._headerLookup;
+ }
+
+ const result = Object.create(null);
for (const [rawName, rawValue] of entries) {
const name = textDecoder.decode(rawName).toLowerCase();
- if (name === targetName) {
- return textDecoder.decode(rawValue);
+ if (DANGEROUS_KEYS.has(name)) {
+ continue;
}
+ result[name] = textDecoder.decode(rawValue);
}
- return undefined;
+ req._headerLookup = result;
+ return result;
}
-// ─── Access Plan ──────────────────────────────────────────────────────────────
+// ─── Access Plan ────────────────────────
function createEmptyAccessPlan() {
return {
@@ -770,6 +770,23 @@ function normalizeHeaderLookup(value) {
return String(value).toLowerCase();
}
+function normalizeHeaderLookupCached(value) {
+ if (typeof value !== "string") {
+ return normalizeHeaderLookup(value);
+ }
+
+ const cached = headerLookupNameCache.get(value);
+ if (cached !== undefined) {
+ return cached;
+ }
+
+ const normalized = value.toLowerCase();
+ if (headerLookupNameCache.size < HEADER_LOOKUP_CACHE_MAX) {
+ headerLookupNameCache.set(value, normalized);
+ }
+ return normalized;
+}
+
function detectJsonFastPath(source) {
if (!source.includes("res.json(")) {
return "fallback";
@@ -800,7 +817,7 @@ function encodeUtf8(value) {
return textEncoder.encode(String(value));
}
-// ─── Binary Protocol Helpers ──────────────────────────────────────────────────
+// ─── Binary Protocol Helpers ────────────
function readBytes(bytes, offset, length) {
if (offset + length > bytes.byteLength) {
diff --git a/src/index.d.ts b/src/index.d.ts
index fd83736..b1b32b6 100644
--- a/src/index.d.ts
+++ b/src/index.d.ts
@@ -91,7 +91,7 @@ export type ErrorHandler = (
res: Response,
) => void | Promise;
-// ─── Listen Options ───────────────────────────────────────────────────────────
+// ─── Listen Options ─────────────────────
export interface HttpServerConfig {
defaultHost?: string;
@@ -116,7 +116,7 @@ export interface ListenOptions {
opt?: Record;
}
-// ─── Server Handle ────────────────────────────────────────────────────────────
+// ─── Server Handle ──────────────────────
export interface ServerHandle {
/** Bound hostname */
@@ -157,7 +157,7 @@ export interface RouteOptimizationInfo {
jsonFastPath?: string;
}
-// ─── Application ──────────────────────────────────────────────────────────────
+// ─── Application ────────────────────────
export interface Application {
/** Register path-scoped or global middleware */
@@ -198,7 +198,7 @@ export interface Application {
/** Create a new http-native application */
export function createApp(): Application;
-// ─── CORS Types ───────────────────────────────────────────────────────────────
+// ─── CORS Types ─────────────────────────
export interface CorsOptions {
/** Allowed origin(s). Default: "*" */
@@ -226,7 +226,7 @@ export interface CorsOptions {
/** Create a CORS middleware */
export function cors(options?: CorsOptions): Middleware;
-// ─── Validation Types ─────────────────────────────────────────────────────────
+// ─── Validation Types ───────────────────
export interface ValidationSchema {
parse(data: unknown): T;
diff --git a/src/index.js b/src/index.js
index a17f06b..d2317be 100644
--- a/src/index.js
+++ b/src/index.js
@@ -35,7 +35,7 @@ const ERROR_REQUEST_PLAN = Object.freeze({
jsonFastPath: "fallback",
});
-// ─── Path Normalization ───────────────────────────────────────────────────────
+// ─── Path Normalization ─────────────────
function normalizePathPrefix(path) {
if (path === "/") {
@@ -240,7 +240,7 @@ function createResponseEnvelope(jsonSerializer = DEFAULT_JSON_SERIALIZER) {
};
}
-// ─── Compiled Middleware Runner ────────────────────────────────────────────────
+// ─── Compiled Middleware Runner ──────────
//
// Generates an optimized runner that avoids function.length checks at runtime
// by pre-classifying middlewares during compilation.
@@ -299,7 +299,7 @@ function createMiddlewareRunner(middlewares) {
};
}
-// ─── Error Handling (Security-Hardened) ───────────────────────────────────────
+// ─── Error Handling (Security-Hardened) ─
function normalizeErrorStatus(error, fallbackStatus = 500) {
const status = Number(error?.status ?? error?.statusCode ?? fallbackStatus);
@@ -320,14 +320,20 @@ function createHttpError(status, message, code) {
function buildDefaultErrorSnapshot(error, fallbackStatus = 500) {
// Security: NEVER leak internal error details to the client
const status = normalizeErrorStatus(error, fallbackStatus);
+ if (status === 404) {
+ return {
+ status,
+ headers: {
+ "content-type": "text/plain; charset=utf-8",
+ },
+ body: Buffer.from("Not Found", "utf8"),
+ };
+ }
+
const isProduction = process.env.NODE_ENV === "production";
let body;
- if (status === 404) {
- body = {
- error: error?.message || "Route not found",
- };
- } else if (status >= 500) {
+ if (status >= 500) {
body = isProduction
? { error: "Internal Server Error" }
: {
@@ -364,7 +370,7 @@ function isPromiseLike(value) {
);
}
-// ─── Dispatcher ───────────────────────────────────────────────────────────────
+// ─── Dispatcher ─────────────────────────
function createDispatcher(compiledRoutes, runtimeOptimizer, errorHandlers = []) {
const routesById = new Map(compiledRoutes.map((route) => [route.handlerId, route]));
@@ -461,7 +467,7 @@ function createDispatcher(compiledRoutes, runtimeOptimizer, errorHandlers = [])
};
}
-// ─── Route Registration & Compilation ─────────────────────────────────────────
+// ─── Route Registration & Compilation ───
function normalizeRouteRegistration(method, path, handler) {
if (typeof handler !== "function") {
@@ -662,7 +668,7 @@ function normalizeListenOptions(options = {}) {
};
}
-// ─── Application Factory ─────────────────────────────────────────────────────
+// ─── Application Factory ───────────────
export function createApp() {
const native = loadNativeModule();
diff --git a/test/app.js b/test/app.js
new file mode 100644
index 0000000..c2bfda6
--- /dev/null
+++ b/test/app.js
@@ -0,0 +1,22 @@
+import { createApp } from "../src/index.js";
+
+const app = createApp();
+
+app.error(async (error, req, res) => {
+ await Promise.resolve();
+ console.error("Error observed in error handler:", error, req, res);
+});
+
+app.get("/", (req, res) => {
+ res.json({
+ ok: true,
+ });
+});
+
+
+
+
+const server = await app.listen({
+ port: 8190
+});
+console.log(`http-native listening on ${server.url}`);