Docs

Runtime differences

Nifra runs the same app.fetch(Request): Response on Bun, Node, Deno and Cloudflare Workers. The runtimes underneath do not behave identically, and none of the differences below is Nifra's doing: they come from the runtimes and their engines. Where Nifra has a switch that decides which behavior an app gets, the entry names it.

Use the same catalog from every surface

nifra runtime is the human CLI and nifra_runtime is the MCP tool. Both read one catalog. Each entry records what you observe, what each runtime does, the fix, how to verify it on your own runtime, and the versions it was measured on (Bun 1.4.4, Node 26.12 and Deno 2.9.8 unless it says otherwise).

Shell
# Browse every difference
nifra runtime

# Filter by runtime, and search by what you observe
nifra runtime deno "path guard skipped"

# Stable JSON for an agent, with versions and categories
nifra runtime node "pipelined requests" --json

# The same catalog over MCP, as text plus structuredContent
nifra_runtime({ runtime: "bun", symptom: "env var undefined" })

Requests

entrywhat differsNifra switch
dot-segment-targetsBun and Workers hand over a resolved URL, so /public/../admin arrives as /admin; Deno and Node hand over the target as sent.normalizeTarget
host-builds-urlDeno builds Request.url from the Host it received, so a Host holding / or # changes the routed path; Bun leaves the URL relative; @nifrajs/node answers 400.validateHost
missing-or-repeated-hostNo Host: a relative URL on Bun, http://localhost/… through @nifrajs/node, http://localhost:port/… on Deno. Two Host lines: Bun uses the first, @nifrajs/node and Deno answer 400.validateHost
pipelined-requestsNode runs every pipelined request's handler at once (30 of 30 in the probe); Bun and Deno run one per connection.requestTimeoutMs, proxy limits
TS
import { server } from "@nifrajs/core"
import { serve } from "@nifrajs/node"

// Deno (and Bun's Host handling): the Host header and the request target are checked and resolved.
const app = server({ validateHost: true, normalizeTarget: true })

// Node: the adapter has the same normalizeTarget switch.
await serve(app, { port: 3000, normalizeTarget: true })

Both switches are off by default because checking costs throughput: about 6% of a bare route for Host validation on Bun, and about 3% for target normalization on Deno. Turn on target normalization when upstream guards and proxies apply a different path policy, or when your path guards run after Nifra normalizes the target. Turn on Host validation when the app builds URLs from the Host header.

Responses and the language

entrywhat differs
json-content-typeResponse.json sends application/json;charset=utf-8 on Bun and application/json on Node and Deno, and Nifra keeps each runtime's value.
engine-error-messagesBun is JavaScriptCore; Node and Deno are V8. JSON.parse, new URL and null reads word their errors differently, so match on the class or a code.
deep-recursionBun runs return f() as a tail call (200,000 deep returned); Node and Deno overflow between 9,000 and 20,000 frames.

Process, files and the edge

entrywhat differs
dotenv-autoloadBun loads .env at startup; Node and Deno need --env-file.
spawn-envBun.spawn without env ignores process.env changes made after startup.
glob-symlinksBun.Glob skips symlinks unless followSymlinks: true.
workerd-node-builtinsWhich node: builtins a Worker gets without nodejs_compat depends on its compatibility_date.

Frontend frameworks and browsers

The browser side has the same property: the behavior belongs to React or the browser, and Nifra only sits on top of it. Those entries live in the frontend gotchas catalog (nifra frontend), next to the per-framework reactivity cases:

  • react-activity-markers: React 19.3.1 hangs hydrating an <Activity> whose boundary comments are missing from the server HTML.
  • csp-inline-style-attribute: what a strict Content-Security-Policy does to inline style attributes, and the <slot> wrapper that avoids it.
  • webkit-streamed-modules: WebKit runs module scripts of a streamed document only after the parser ends.

Keeping it true

A runtime release can change any entry. The catalog test checks the Bun claims for JSON parsing, JSON content type, recursion depth and URL errors against the Bun it runs on; the other entries are recorded observations. To add or fix an entry, reproduce it on each runtime, record the versions, and keep the cause to what the runtime does.