diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 81ac91a..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,7 +29,6 @@ on:
description: "Bombardier timeout"
required: false
default: "2s"
-
jobs:
benchmark:
runs-on: ubuntu-latest
@@ -46,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
@@ -59,14 +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' }}"
+ --timeout="${{ github.event.inputs.timeout || '2s' }}" \
+ --http-native-runtime="node" \
+ --output-dir="bench/results/node"
- name: Upload benchmark artifacts
if: always()
@@ -74,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/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..a7b0502
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,24 @@
+# Http-native is opensource and freely available for everyone.
+
+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.
+
+A commit would like this,
+
+opt: Fix benchmarks to be more optimized.
+chore: removed ugly comments
+rm: Removed all deprecated code
+other: added a contributing.md
+
+You are allowed to mix commit types via & as seen in 'opt&chore:'.
+
+Please don't push code that's below the current benchmarks. Don't push testing code.
+
+Contact Us
+You can find us in a couple places online. First and foremost, we're active right here on GitHub. If you encounter a bug or other problems, open an issue on here for us to take a look at it. We also accept feature requests here as well.
+
+- Http-native (Nadhi)
diff --git a/bench/ci.js b/bench/ci.js
index d328d8d..c9c07ff 100644
--- a/bench/ci.js
+++ b/bench/ci.js
@@ -5,13 +5,15 @@ 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";
const DEFAULT_TIMEOUT = "2s";
const DEFAULT_OUTPUT_DIR = "bench/results";
+const DEFAULT_HTTP_NATIVE_RUNTIME = "bun";
const DEFAULT_BOMBARDIER_BIN = resolveBombardierBin();
+const SUPPORTED_HTTP_NATIVE_RUNTIMES = new Set(["bun", "node"]);
const SERVER_PORTS = Object.freeze({
bun: { static: 3000, dynamic: 3010, opt: 3020 },
@@ -59,6 +61,7 @@ async function main() {
connections: options.connections,
duration: options.duration,
timeout: options.timeout,
+ httpNativeRuntime: options.httpNativeRuntime,
},
results,
};
@@ -130,6 +133,15 @@ function parseArgs(argv) {
const duration = values.get("duration") ?? DEFAULT_DURATION;
const timeout = values.get("timeout") ?? DEFAULT_TIMEOUT;
const outputDir = values.get("output-dir") ?? DEFAULT_OUTPUT_DIR;
+ const httpNativeRuntime = (values.get("http-native-runtime") ?? DEFAULT_HTTP_NATIVE_RUNTIME).trim();
+
+ if (!SUPPORTED_HTTP_NATIVE_RUNTIMES.has(httpNativeRuntime)) {
+ throw new Error(
+ `Unsupported --http-native-runtime \"${httpNativeRuntime}\". Supported runtimes: ${[
+ ...SUPPORTED_HTTP_NATIVE_RUNTIMES,
+ ].join(", ")}`,
+ );
+ }
return {
engines,
@@ -138,6 +150,7 @@ function parseArgs(argv) {
duration,
timeout,
outputDir,
+ httpNativeRuntime,
};
}
@@ -145,11 +158,14 @@ 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`);
console.log(` --timeout=${DEFAULT_TIMEOUT} Bombardier timeout`);
+ console.log(
+ ` --http-native-runtime=${DEFAULT_HTTP_NATIVE_RUNTIME} Runtime for http-native/old: bun | node`,
+ );
console.log(` --output-dir=${DEFAULT_OUTPUT_DIR} Where to write results.json and summary.md`);
}
@@ -212,7 +228,7 @@ function portFor(engine, scenario) {
async function runBenchmarkCase(testCase, options) {
console.log(`[http-native][bench] starting ${testCase.engine}/${testCase.scenario} on :${testCase.port}`);
- const server = spawnServer(testCase);
+ const server = spawnServer(testCase, options);
const serverLogs = [];
let readyResolve;
const ready = new Promise((resolve) => {
@@ -308,9 +324,10 @@ async function runBenchmarkCase(testCase, options) {
}
}
-function spawnServer(testCase) {
+function spawnServer(testCase, options) {
if (testCase.engine === "bun" || testCase.engine === "http-native" || testCase.engine === "old") {
- return spawn("bun", ["bench/target.js", testCase.engine, testCase.scenario, String(testCase.port)], {
+ const runtime = testCase.engine === "bun" ? "bun" : options.httpNativeRuntime;
+ return spawn(runtime, ["bench/target.js", testCase.engine, testCase.scenario, String(testCase.port)], {
cwd: process.cwd(),
detached: process.platform !== "win32",
stdio: ["ignore", "pipe", "pipe"],
diff --git a/bench/run.js b/bench/run.js
index 828586f..cd81d59 100644
--- a/bench/run.js
+++ b/bench/run.js
@@ -1,19 +1,22 @@
import { spawn } from "node:child_process";
import { once } from "node:events";
-const [, , engineArg, scenarioArg, portArg] = process.argv;
+const [, , engineArg, scenarioArg, portArg, runtimeArg] = process.argv;
const engine = engineArg ?? "http-native";
const scenario = scenarioArg ?? "static";
const port = Number(portArg ?? 3001);
+const httpNativeRuntime = runtimeArg ?? "bun";
function printUsage() {
- console.log("Usage: bun bench/run.js ");
+ console.log("Usage: bun bench/run.js [httpNativeRuntime]");
console.log("Engines: http-native | bun | fiber | xitca | monoio | zig");
console.log("Scenarios: static | dynamic | opt");
+ console.log("httpNativeRuntime: bun | node (only applies to http-native and old)");
console.log("");
console.log("Example:");
- console.log(" bun bench/run.js http-native static 3001");
+ console.log(" bun bench/run.js http-native static 3001 bun");
+ console.log(" bun bench/run.js http-native static 3001 node");
console.log(" bombardier -c 200 -d 10s http://127.0.0.1:3001/");
}
@@ -40,6 +43,13 @@ async function main() {
process.exit(1);
}
+ if (!["bun", "node"].includes(httpNativeRuntime)) {
+ printUsage();
+ process.exit(1);
+ }
+
+ const targetRuntime = engine === "http-native" || engine === "old" ? httpNativeRuntime : "bun";
+
const child =
engine === "xitca" || engine === "monoio"
? spawn(
@@ -74,7 +84,7 @@ async function main() {
cwd: process.cwd(),
stdio: ["ignore", "pipe", "inherit"],
})
- : spawn("bun", ["bench/target.js", engine, scenario, String(port)], {
+ : spawn(targetRuntime, ["bench/target.js", engine, scenario, String(port)], {
cwd: process.cwd(),
stdio: ["ignore", "pipe", "inherit"],
});
diff --git a/bench/target.js b/bench/target.js
index 3a3ed34..6c88a48 100644
--- a/bench/target.js
+++ b/bench/target.js
@@ -1,6 +1,7 @@
import { resolve } from "node:path";
process.env.HTTP_NATIVE_NODE_PATH ??= resolve(process.cwd(), "http-native.release.node");
+process.env.HTTP_NATIVE_NATIVE_PATH ??= process.env.HTTP_NATIVE_NODE_PATH;
const { createApp: createHttpNativeApp } = await import("../src/index.js");
@@ -124,7 +125,7 @@ async function startFrameworkServer(createApp, label, activeScenario) {
port,
opt:
label === "http-native" && activeScenario === "opt"
- ? { notify: true }
+ ? { notify: true, cache: true }
: undefined,
});
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/error-handling/server.js b/examples/error-handling/server.js
index f219d36..bd79a99 100644
--- a/examples/error-handling/server.js
+++ b/examples/error-handling/server.js
@@ -2,7 +2,6 @@ import { createApp } from "http-native";
const app = createApp();
-// ─── Custom Error Classes ─────────────────────────────────────────────────────
class AppError extends Error {
constructor(message, statusCode = 500, code = "INTERNAL_ERROR") {
@@ -30,7 +29,7 @@ class UnauthorizedError extends AppError {
}
}
-// ─── Global Error Handler ─────────────────────────────────────────────────────
+// Global error handler
app.onError((err, req, res) => {
// Known application errors
@@ -58,7 +57,7 @@ app.onError((err, req, res) => {
});
});
-// ─── Routes ───────────────────────────────────────────────────────────────────
+// Routes.
app.get("/", (req, res) => {
res.json({ status: "ok" });
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/noslop/AGENTS.md b/noslop/AGENTS.md
new file mode 100644
index 0000000..e69de29
diff --git a/noslop/CLAUDE.md b/noslop/CLAUDE.md
new file mode 100644
index 0000000..e69de29
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/package.json b/package.json
index a92e760..224abce 100644
--- a/package.json
+++ b/package.json
@@ -18,18 +18,24 @@
"test": "bun run build && bun test/test.js",
"bench": "bun run build:release && bun bench/run.js",
"bench:http-native:static": "bun run build:release && bun bench/run.js http-native static 3001",
+ "bench:http-native:bun:static": "bun run build:release && bun bench/run.js http-native static 3001 bun",
+ "bench:http-native:node:static": "bun run build:release && bun bench/run.js http-native static 3001 node",
"bench:bun:static": "bun bench/run.js bun static 3000",
"bench:fiber:static": "bun bench/run.js fiber static 3009",
"bench:xitca:static": "bun bench/run.js xitca static 3003",
"bench:monoio:static": "bun bench/run.js monoio static 3004",
"bench:zig:static": "bun bench/run.js zig static 3005",
"bench:http-native:dynamic": "bun run build:release && bun bench/run.js http-native dynamic 3011",
+ "bench:http-native:bun:dynamic": "bun run build:release && bun bench/run.js http-native dynamic 3011 bun",
+ "bench:http-native:node:dynamic": "bun run build:release && bun bench/run.js http-native dynamic 3011 node",
"bench:bun:dynamic": "bun bench/run.js bun dynamic 3010",
"bench:fiber:dynamic": "bun bench/run.js fiber dynamic 3019",
"bench:xitca:dynamic": "bun bench/run.js xitca dynamic 3013",
"bench:monoio:dynamic": "bun bench/run.js monoio dynamic 3014",
"bench:zig:dynamic": "bun bench/run.js zig dynamic 3015",
"bench:http-native:opt": "bun run build:release && bun bench/run.js http-native opt 3021",
+ "bench:http-native:bun:opt": "bun run build:release && bun bench/run.js http-native opt 3021 bun",
+ "bench:http-native:node:opt": "bun run build:release && bun bench/run.js http-native opt 3021 node",
"bench:bun:opt": "bun bench/run.js bun opt 3020",
"bench:fiber:opt": "bun bench/run.js fiber opt 3029",
"bench:xitca:opt": "bun bench/run.js xitca opt 3023",
diff --git a/readme.md b/readme.md
index 9c2295c..d446991 100644
--- a/readme.md
+++ b/readme.md
@@ -2,6 +2,7 @@
+
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/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/analyzer.rs b/rust-native/src/analyzer.rs
index 74e908f..9e53c82 100644
--- a/rust-native/src/analyzer.rs
+++ b/rust-native/src/analyzer.rs
@@ -3,10 +3,6 @@ use std::collections::HashMap;
use crate::manifest::{MiddlewareInput, RouteInput};
-/// Optimize static stuff
-/// Its not very fun but we don't need to call js again and again for
-/// static responses, we can just return them from rust
-
#[derive(Clone)]
pub struct StaticResponseSpec {
pub status: u16,
@@ -14,6 +10,66 @@ pub struct StaticResponseSpec {
pub body: Vec,
}
+#[derive(Clone)]
+pub struct DynamicFastPathSpec {
+ pub status: u16,
+ pub headers: Box<[(Box, Box)]>,
+ pub response: DynamicFastPathResponse,
+}
+
+#[derive(Clone)]
+pub enum DynamicFastPathResponse {
+ Json(JsonTemplate),
+ Text(TextTemplate),
+}
+
+#[derive(Clone)]
+pub struct JsonTemplate {
+ pub kind: JsonTemplateKind,
+}
+
+#[derive(Clone)]
+pub enum JsonTemplateKind {
+ Object(Box<[JsonObjectField]>),
+ Literal(Box<[u8]>),
+}
+
+#[derive(Clone)]
+pub struct JsonObjectField {
+ pub key_prefix: Box<[u8]>,
+ pub value: JsonValueTemplate,
+}
+
+#[derive(Clone)]
+pub enum JsonValueTemplate {
+ Literal(Box<[u8]>),
+ Dynamic(DynamicValueSource),
+}
+
+#[derive(Clone)]
+pub struct TextTemplate {
+ pub segments: Box<[TextSegment]>,
+}
+
+#[derive(Clone)]
+pub enum TextSegment {
+ Literal(Box),
+ Dynamic(DynamicValueSource),
+}
+
+#[derive(Clone)]
+pub struct DynamicValueSource {
+ pub kind: DynamicValueSourceKind,
+ pub key: Box,
+}
+
+#[derive(Clone, Copy, Eq, PartialEq)]
+pub enum DynamicValueSourceKind {
+ Param,
+ Query,
+ Header,
+}
+
pub enum AnalysisResult {
ExactStaticFastPath(StaticResponseSpec),
Dynamic,
@@ -65,6 +121,51 @@ pub fn analyze_route(route: &RouteInput, middlewares: &[MiddlewareInput]) -> Ana
AnalysisResult::Dynamic
}
+pub fn analyze_dynamic_fast_path(
+ route: &RouteInput,
+ middlewares: &[MiddlewareInput],
+) -> Option {
+ if has_applicable_middleware(route.path.as_str(), middlewares) {
+ return None;
+ }
+
+ let source = route.handler_source.as_str();
+ if source.contains("await") {
+ return None;
+ }
+
+ let body = trim_return_and_semicolon(extract_function_body(source));
+ if body.is_empty() {
+ return None;
+ }
+
+ if let Some((status, payload)) = parse_status_call(body, "json") {
+ return build_dynamic_json_response(status, payload);
+ }
+
+ if let Some((status, payload)) = parse_status_call(body, "send") {
+ return build_dynamic_send_response(status, payload, None);
+ }
+
+ if let Some((status, content_type, payload)) = parse_status_type_send_call(body) {
+ return build_dynamic_send_response(status, payload, Some(content_type));
+ }
+
+ if let Some((content_type, payload)) = parse_type_send_call(body) {
+ return build_dynamic_send_response(200, payload, Some(content_type));
+ }
+
+ if let Some(payload) = strip_call(body, "res.json(") {
+ return build_dynamic_json_response(200, payload);
+ }
+
+ if let Some(payload) = strip_call(body, "res.send(") {
+ return build_dynamic_send_response(200, payload, None);
+ }
+
+ None
+}
+
pub fn normalize_path(path: &str) -> String {
if path == "/" {
return "/".to_string();
@@ -167,6 +268,42 @@ fn parse_status_call<'a>(body: &'a str, method: &str) -> Option<(u16, &'a str)>
Some((status, payload.trim()))
}
+fn parse_status_type_send_call(body: &str) -> Option<(u16, String, &str)> {
+ let status_prefix = "res.status(";
+ let type_separator = ").type(";
+ let send_separator = ").send(";
+
+ if !body.starts_with(status_prefix) || !body.ends_with(')') {
+ return None;
+ }
+
+ let after_status = &body[status_prefix.len()..];
+ let type_index = after_status.find(type_separator)?;
+ let status = after_status[..type_index].trim().parse::().ok()?;
+ let after_type = &after_status[type_index + type_separator.len()..];
+ let send_index = after_type.find(send_separator)?;
+ let content_type = parse_string_literal(after_type[..send_index].trim())?;
+ let payload_start = send_index + send_separator.len();
+ let payload = &after_type[payload_start..after_type.len() - 1];
+ Some((status, content_type, payload.trim()))
+}
+
+fn parse_type_send_call(body: &str) -> Option<(String, &str)> {
+ let type_prefix = "res.type(";
+ let send_separator = ").send(";
+
+ if !body.starts_with(type_prefix) || !body.ends_with(')') {
+ return None;
+ }
+
+ let after_type = &body[type_prefix.len()..];
+ let send_index = after_type.find(send_separator)?;
+ let content_type = parse_string_literal(after_type[..send_index].trim())?;
+ let payload_start = send_index + send_separator.len();
+ let payload = &after_type[payload_start..after_type.len() - 1];
+ Some((content_type, payload.trim()))
+}
+
fn strip_call<'a>(body: &'a str, prefix: &str) -> Option<&'a str> {
if !body.starts_with(prefix) || !body.ends_with(')') {
return None;
@@ -191,6 +328,90 @@ fn build_json_response(status: u16, payload: &str) -> Option
})
}
+fn build_dynamic_json_response(status: u16, payload: &str) -> Option {
+ let template = parse_json_template(payload)?;
+ let headers = [(
+ "content-type".to_string(),
+ "application/json; charset=utf-8".to_string(),
+ )];
+
+ Some(DynamicFastPathSpec {
+ status,
+ headers: headers
+ .into_iter()
+ .map(|(name, value)| (name.into_boxed_str(), value.into_boxed_str()))
+ .collect::>()
+ .into_boxed_slice(),
+ response: DynamicFastPathResponse::Json(template),
+ })
+}
+
+fn build_dynamic_send_response(
+ status: u16,
+ payload: &str,
+ forced_content_type: Option,
+) -> Option {
+ if let Some(text_template) = parse_text_template(payload) {
+ let content_type = forced_content_type
+ .map(normalize_content_type)
+ .unwrap_or_else(|| "text/plain; charset=utf-8".to_string());
+ let headers = [("content-type".to_string(), content_type)];
+
+ return Some(DynamicFastPathSpec {
+ status,
+ headers: headers
+ .into_iter()
+ .map(|(name, value)| (name.into_boxed_str(), value.into_boxed_str()))
+ .collect::>()
+ .into_boxed_slice(),
+ response: DynamicFastPathResponse::Text(text_template),
+ });
+ }
+
+ let value = parse_literal(payload)?;
+ match value {
+ Value::String(text) => {
+ let content_type = forced_content_type
+ .map(normalize_content_type)
+ .unwrap_or_else(|| "text/plain; charset=utf-8".to_string());
+ let headers = [("content-type".to_string(), content_type)];
+ Some(DynamicFastPathSpec {
+ status,
+ headers: headers
+ .into_iter()
+ .map(|(name, value)| (name.into_boxed_str(), value.into_boxed_str()))
+ .collect::>()
+ .into_boxed_slice(),
+ response: DynamicFastPathResponse::Text(TextTemplate {
+ segments: vec![TextSegment::Literal(text.into_boxed_str())].into_boxed_slice(),
+ }),
+ })
+ }
+ Value::Null => None,
+ other => {
+ if forced_content_type.is_some() {
+ return None;
+ }
+ let body = serde_json::to_vec(&other).ok()?.into_boxed_slice();
+ let headers = [(
+ "content-type".to_string(),
+ "application/json; charset=utf-8".to_string(),
+ )];
+ Some(DynamicFastPathSpec {
+ status,
+ headers: headers
+ .into_iter()
+ .map(|(name, value)| (name.into_boxed_str(), value.into_boxed_str()))
+ .collect::>()
+ .into_boxed_slice(),
+ response: DynamicFastPathResponse::Json(JsonTemplate {
+ kind: JsonTemplateKind::Literal(body),
+ }),
+ })
+ }
+ }
+}
+
fn build_send_response(status: u16, payload: &str) -> Option {
let value = parse_literal(payload)?;
match value {
@@ -224,6 +445,414 @@ fn build_send_response(status: u16, payload: &str) -> Option
}
}
+fn parse_json_template(payload: &str) -> Option {
+ let payload = payload.trim();
+
+ if payload.starts_with('{') && payload.ends_with('}') {
+ let fields = parse_json_object_fields(payload)?;
+ return Some(JsonTemplate {
+ kind: JsonTemplateKind::Object(fields.into_boxed_slice()),
+ });
+ }
+
+ let literal = parse_literal(payload)?;
+ let literal_bytes = serde_json::to_vec(&literal).ok()?.into_boxed_slice();
+ Some(JsonTemplate {
+ kind: JsonTemplateKind::Literal(literal_bytes),
+ })
+}
+
+fn parse_json_object_fields(payload: &str) -> Option> {
+ let inner = payload[1..payload.len() - 1].trim();
+ if inner.is_empty() {
+ return Some(Vec::new());
+ }
+
+ let entries = split_top_level(inner, ',')?;
+ let mut fields = Vec::with_capacity(entries.len());
+
+ for entry in entries {
+ let trimmed = entry.trim();
+ if trimmed.is_empty() {
+ continue;
+ }
+
+ let separator = find_top_level(trimmed, ':')?;
+ let key = parse_object_key(trimmed[..separator].trim())?;
+ let value_source = trimmed[separator + 1..].trim();
+ let value = if let Some(source) = parse_dynamic_value_source(value_source) {
+ JsonValueTemplate::Dynamic(source)
+ } else {
+ let literal = parse_literal(value_source)?;
+ JsonValueTemplate::Literal(serde_json::to_vec(&literal).ok()?.into_boxed_slice())
+ };
+
+ let key_prefix = format!("{}:", serde_json::to_string(&key).ok()?).into_bytes();
+ fields.push(JsonObjectField {
+ key_prefix: key_prefix.into_boxed_slice(),
+ value,
+ });
+ }
+
+ Some(fields)
+}
+
+fn parse_object_key(raw: &str) -> Option {
+ let raw = raw.trim();
+ if raw.is_empty() {
+ return None;
+ }
+
+ if let Some(value) = parse_string_literal(raw) {
+ return Some(value);
+ }
+
+ if is_identifier(raw) {
+ return Some(raw.to_string());
+ }
+
+ None
+}
+
+fn parse_text_template(payload: &str) -> Option {
+ let payload = payload.trim();
+
+ if let Some(string_value) = parse_string_literal(payload) {
+ return Some(TextTemplate {
+ segments: vec![TextSegment::Literal(string_value.into_boxed_str())].into_boxed_slice(),
+ });
+ }
+
+ parse_template_literal(payload)
+}
+
+fn parse_template_literal(payload: &str) -> Option {
+ if !payload.starts_with('`') || !payload.ends_with('`') || payload.len() < 2 {
+ return None;
+ }
+
+ let inner = &payload[1..payload.len() - 1];
+ if !inner.contains("${") {
+ return Some(TextTemplate {
+ segments: vec![TextSegment::Literal(
+ unescape_template_literal(inner)?.into_boxed_str(),
+ )]
+ .into_boxed_slice(),
+ });
+ }
+
+ let mut segments = Vec::new();
+ let mut cursor = 0usize;
+ let bytes = inner.as_bytes();
+
+ while cursor < bytes.len() {
+ let mut expr_start = None;
+ let mut index = cursor;
+ while index + 1 < bytes.len() {
+ if bytes[index] == b'$' && bytes[index + 1] == b'{' {
+ expr_start = Some(index);
+ break;
+ }
+ index += 1;
+ }
+
+ let Some(start) = expr_start else {
+ let tail = unescape_template_literal(&inner[cursor..])?;
+ if !tail.is_empty() {
+ segments.push(TextSegment::Literal(tail.into_boxed_str()));
+ }
+ break;
+ };
+
+ if start > cursor {
+ let literal = unescape_template_literal(&inner[cursor..start])?;
+ if !literal.is_empty() {
+ segments.push(TextSegment::Literal(literal.into_boxed_str()));
+ }
+ }
+
+ let expr_end = find_template_expr_end(inner, start + 2)?;
+ let expression = inner[start + 2..expr_end].trim();
+ let source = parse_dynamic_value_source(expression)?;
+ segments.push(TextSegment::Dynamic(source));
+ cursor = expr_end + 1;
+ }
+
+ Some(TextTemplate {
+ segments: segments.into_boxed_slice(),
+ })
+}
+
+fn find_template_expr_end(input: &str, start: usize) -> Option {
+ let bytes = input.as_bytes();
+ let mut depth = 0usize;
+ let mut index = start;
+ let mut string_delim: Option = None;
+ let mut escaped = false;
+
+ while index < bytes.len() {
+ let byte = bytes[index];
+ if let Some(delim) = string_delim {
+ if escaped {
+ escaped = false;
+ } else if byte == b'\\' {
+ escaped = true;
+ } else if byte == delim {
+ string_delim = None;
+ }
+ index += 1;
+ continue;
+ }
+
+ match byte {
+ b'\'' | b'"' | b'`' => {
+ string_delim = Some(byte);
+ }
+ b'{' => {
+ depth += 1;
+ }
+ b'}' => {
+ if depth == 0 {
+ return Some(index);
+ }
+ depth -= 1;
+ }
+ _ => {}
+ }
+
+ index += 1;
+ }
+
+ None
+}
+
+fn parse_dynamic_value_source(raw: &str) -> Option {
+ let source = raw.trim();
+ if let Some(key) = source.strip_prefix("req.params.") {
+ if is_identifier(key) {
+ return Some(DynamicValueSource {
+ kind: DynamicValueSourceKind::Param,
+ key: key.to_string().into_boxed_str(),
+ });
+ }
+ }
+
+ if let Some(key) = parse_bracket_access(source, "req.params[") {
+ return Some(DynamicValueSource {
+ kind: DynamicValueSourceKind::Param,
+ key: key.into_boxed_str(),
+ });
+ }
+
+ if let Some(key) = source.strip_prefix("req.query.") {
+ if is_identifier(key) {
+ return Some(DynamicValueSource {
+ kind: DynamicValueSourceKind::Query,
+ key: key.to_string().into_boxed_str(),
+ });
+ }
+ }
+
+ if let Some(key) = parse_bracket_access(source, "req.query[") {
+ return Some(DynamicValueSource {
+ kind: DynamicValueSourceKind::Query,
+ key: key.into_boxed_str(),
+ });
+ }
+
+ if let Some(key) = source.strip_prefix("req.headers.") {
+ if is_identifier(key) {
+ return Some(DynamicValueSource {
+ kind: DynamicValueSourceKind::Header,
+ key: key.to_ascii_lowercase().into_boxed_str(),
+ });
+ }
+ }
+
+ if let Some(key) = parse_bracket_access(source, "req.headers[") {
+ return Some(DynamicValueSource {
+ kind: DynamicValueSourceKind::Header,
+ key: key.to_ascii_lowercase().into_boxed_str(),
+ });
+ }
+
+ if let Some(key) = parse_function_call_string_arg(source, "req.header(") {
+ return Some(DynamicValueSource {
+ kind: DynamicValueSourceKind::Header,
+ key: key.to_ascii_lowercase().into_boxed_str(),
+ });
+ }
+
+ None
+}
+
+fn parse_function_call_string_arg(source: &str, prefix: &str) -> Option {
+ if !source.starts_with(prefix) || !source.ends_with(')') {
+ return None;
+ }
+
+ parse_string_literal(source[prefix.len()..source.len() - 1].trim())
+}
+
+fn parse_bracket_access(source: &str, prefix: &str) -> Option {
+ if !source.starts_with(prefix) || !source.ends_with(']') {
+ return None;
+ }
+
+ parse_string_literal(source[prefix.len()..source.len() - 1].trim())
+}
+
+fn parse_string_literal(source: &str) -> Option {
+ let value = parse_literal(source)?;
+ match value {
+ Value::String(text) => Some(text),
+ _ => None,
+ }
+}
+
+fn split_top_level<'a>(input: &'a str, separator: char) -> Option> {
+ let mut values = Vec::new();
+ let mut start = 0usize;
+ let mut brace_depth = 0usize;
+ let mut bracket_depth = 0usize;
+ let mut paren_depth = 0usize;
+ let mut string_delimiter: Option = None;
+ let mut escaped = false;
+
+ for (index, ch) in input.char_indices() {
+ if let Some(delimiter) = string_delimiter {
+ if escaped {
+ escaped = false;
+ continue;
+ }
+ if ch == '\\' {
+ escaped = true;
+ continue;
+ }
+ if ch == delimiter {
+ string_delimiter = None;
+ }
+ continue;
+ }
+
+ match ch {
+ '"' | '\'' | '`' => string_delimiter = Some(ch),
+ '{' => brace_depth += 1,
+ '}' => brace_depth = brace_depth.checked_sub(1)?,
+ '[' => bracket_depth += 1,
+ ']' => bracket_depth = bracket_depth.checked_sub(1)?,
+ '(' => paren_depth += 1,
+ ')' => paren_depth = paren_depth.checked_sub(1)?,
+ _ if ch == separator && brace_depth == 0 && bracket_depth == 0 && paren_depth == 0 => {
+ values.push(&input[start..index]);
+ start = index + ch.len_utf8();
+ }
+ _ => {}
+ }
+ }
+
+ if string_delimiter.is_some() || brace_depth != 0 || bracket_depth != 0 || paren_depth != 0 {
+ return None;
+ }
+
+ values.push(&input[start..]);
+ Some(values)
+}
+
+fn find_top_level(input: &str, target: char) -> Option {
+ let mut brace_depth = 0usize;
+ let mut bracket_depth = 0usize;
+ let mut paren_depth = 0usize;
+ let mut string_delimiter: Option = None;
+ let mut escaped = false;
+
+ for (index, ch) in input.char_indices() {
+ if let Some(delimiter) = string_delimiter {
+ if escaped {
+ escaped = false;
+ continue;
+ }
+ if ch == '\\' {
+ escaped = true;
+ continue;
+ }
+ if ch == delimiter {
+ string_delimiter = None;
+ }
+ continue;
+ }
+
+ match ch {
+ '"' | '\'' | '`' => string_delimiter = Some(ch),
+ '{' => brace_depth += 1,
+ '}' => brace_depth = brace_depth.checked_sub(1)?,
+ '[' => bracket_depth += 1,
+ ']' => bracket_depth = bracket_depth.checked_sub(1)?,
+ '(' => paren_depth += 1,
+ ')' => paren_depth = paren_depth.checked_sub(1)?,
+ _ if ch == target && brace_depth == 0 && bracket_depth == 0 && paren_depth == 0 => {
+ return Some(index);
+ }
+ _ => {}
+ }
+ }
+
+ None
+}
+
+fn is_identifier(value: &str) -> bool {
+ let mut chars = value.chars();
+ let Some(first) = chars.next() else {
+ return false;
+ };
+
+ if !(first.is_ascii_alphabetic() || first == '_' || first == '$') {
+ return false;
+ }
+
+ chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '$')
+}
+
+fn normalize_content_type(value: String) -> String {
+ if value.contains('/') {
+ return value;
+ }
+
+ match value.as_str() {
+ "json" => "application/json; charset=utf-8".to_string(),
+ "html" => "text/html; charset=utf-8".to_string(),
+ "text" => "text/plain; charset=utf-8".to_string(),
+ _ => value,
+ }
+}
+
+fn unescape_template_literal(input: &str) -> Option {
+ let mut output = String::with_capacity(input.len());
+ let mut chars = input.chars();
+ while let Some(ch) = chars.next() {
+ if ch != '\\' {
+ output.push(ch);
+ continue;
+ }
+
+ let Some(next) = chars.next() else {
+ return None;
+ };
+ match next {
+ '\\' => output.push('\\'),
+ '`' => output.push('`'),
+ '"' => output.push('"'),
+ '\'' => output.push('\''),
+ 'n' => output.push('\n'),
+ 'r' => output.push('\r'),
+ 't' => output.push('\t'),
+ '$' => output.push('$'),
+ other => output.push(other),
+ }
+ }
+ Some(output)
+}
+
fn parse_literal(source: &str) -> Option {
let normalized = normalize_js_literal(source);
json5::from_str::(normalized.as_str()).ok()
diff --git a/rust-native/src/lib.rs b/rust-native/src/lib.rs
index f9c0e58..69f5487 100644
--- a/rust-native/src/lib.rs
+++ b/rust-native/src/lib.rs
@@ -16,11 +16,16 @@ use std::cell::RefCell;
use std::net::{SocketAddr, ToSocketAddrs};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{mpsc, Arc, Mutex};
+use url::form_urlencoded;
+use crate::analyzer::{
+ DynamicFastPathResponse, DynamicValueSourceKind, JsonTemplateKind, JsonValueTemplate,
+ TextSegment,
+};
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";
@@ -34,8 +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;
-const NOT_FOUND_BODY: &[u8] = br#"{"error":"Route not found"}"#;
/// Security: Maximum number of headers we allow per request
const MAX_HEADER_COUNT: usize = 64;
@@ -55,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.
@@ -84,7 +90,7 @@ fn release_buffer(mut buf: Vec) {
});
}
-// ─── Server Configuration ─────────────────────────────────────────────────────
+// ─── Server Configuration ───────────────
#[derive(Clone)]
struct HttpServerConfig {
@@ -162,7 +168,7 @@ impl HttpServerConfig {
}
}
-// ─── NAPI Interface ───────────────────────────────────────────────────────────
+// ─── NAPI Interface ─────────────────────
#[napi(object)]
pub struct NativeListenOptions {
@@ -360,7 +366,7 @@ fn worker_count_for(options: &NativeListenOptions) -> usize {
.unwrap_or(1)
}
-// ─── JS Dispatcher ────────────────────────────────────────────────────────────
+// ─── JS Dispatcher ──────────────────────
struct JsDispatcher {
callback: DispatchTsfn,
@@ -380,7 +386,7 @@ impl JsDispatcher {
}
}
-// ─── Server Loop ──────────────────────────────────────────────────────────────
+// ─── Server Loop ────────────────────────
async fn run_server(
listener: TcpListener,
@@ -429,7 +435,7 @@ async fn run_server(
Ok(())
}
-// ─── Parsed Request (from httparse) ───────────────────────────────────────────
+// ─── Parsed Request (from httparse) ─────
struct ParsedRequest<'a> {
method: &'a [u8],
@@ -443,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,
@@ -522,22 +528,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 +542,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 +554,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_decision = build_dispatch_decision_zero_copy(router, &parsed, &[])?;
+ drop(parsed);
+ drain_consumed_bytes(buffer, header_bytes);
+
+ match dispatch_decision {
+ DispatchDecision::BridgeRequest(request) => {
+ write_dynamic_dispatch_response(stream, dispatcher, request, keep_alive)
+ .await?;
+ }
+ DispatchDecision::SpecializedResponse(response) => {
+ let (write_result, _) = stream.write_all(response).await;
+ write_result?;
+ }
+ }
+
+ 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 +605,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 +614,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 +621,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 +637,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,
@@ -631,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?;
@@ -647,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
@@ -740,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.
@@ -816,12 +833,77 @@ fn parse_hot_root_request(
})
}
-// ─── Routing ──────────────────────────────────────────────────────────────────
+// ─── Routing ────────────────────────────
// ─── Bridge Envelope Building (Single-Pass Headers) ───────────────────────────
//
// 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.).
+enum DispatchDecision {
+ BridgeRequest(Buffer),
+ SpecializedResponse(Vec),
+}
+
+fn build_dispatch_decision_zero_copy(
+ router: &Router,
+ parsed: &ParsedRequest<'_>,
+ body: &[u8],
+) -> Result {
+ 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 build_not_found_dispatch_envelope(
+ method_code,
+ path_str,
+ url_str,
+ &parsed.headers,
+ body,
+ )
+ .map(DispatchDecision::BridgeRequest);
+ }
+
+ 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,
+ url_str,
+ &parsed.headers,
+ body,
+ )
+ .map(DispatchDecision::BridgeRequest);
+ };
+
+ if let Some(response) =
+ build_dynamic_fast_path_response(&matched_route, url_str, &parsed.headers, parsed.keep_alive)?
+ {
+ return Ok(DispatchDecision::SpecializedResponse(response));
+ };
+
+ build_dispatch_envelope(
+ &matched_route,
+ method_code,
+ path_str,
+ url_str,
+ &parsed.headers,
+ body,
+ )
+ .map(DispatchDecision::BridgeRequest)
+}
+
fn build_dispatch_request_owned(
router: &Router,
method: &[u8],
@@ -829,40 +911,45 @@ fn build_dispatch_request_owned(
path: &[u8],
headers: &[(String, String)],
body: &[u8],
-) -> Result