Observed since 20 Oct, 17:41 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
0http-bun
0http for Bun
A high-performance, minimalist HTTP framework for Bun, inspired by 0http. Built specifically to leverage Bun's native performance capabilities with a developer-friendly API.
Landing page: 0http-bun.21no.de
v1.3.0 includes a comprehensive security hardening release. See the Changelog and Migration Guide sections below.
0http-bun combines the simplicity of Express with the raw performance of Bun's runtime, delivering a framework that's both blazingly fast and secure by design. Perfect for everything from quick prototypes to production-grade APIs.
trouter library with intelligent cachingfast-querystring for lightning-quick query string handlingbun add 0http-bun
import http from '0http-bun'
const {router} = http()
router.get('/', () => {
return new Response('Hello World!')
})
router.get('/:id', (req) => {
return Response.json({id: req.params.id})
})
// Start the server
Bun.serve({
port: 3000,
fetch: router.fetch,
})
Bun's standard Request object does not expose the client IP address. To enable the default rate limiter key generator (and any middleware that reads req.ip), use server.requestIP() in the Bun.serve fetch handler:
import http from '0http-bun'
import {createRateLimit} from '0http-bun/lib/middleware'
const {router} = http()
router.use(createRateLimit({windowMs: 15 * 60 * 1000, max: 100}))
router.get('/', () => new Response('Hello World!'))
// Populate req.ip from Bun's server.requestIP before passing to the router
Bun.serve({
port: 3000,
fetch(req, server) {
req.ip = server.requestIP(req)?.address
return router.fetch(req)
},
})
Why is this needed? The Fetch API
Requesttype does not include connection-level properties likeip. Bun provides client IP viaserver.requestIP(req)in the fetch handler's second argument. Settingreq.ipbefore callingrouter.fetchensures the rate limiter (and other middleware) can identify clients correctly. Without this, the default key generator falls back to unique per-request keys, which effectively disables rate limiting.
import http, {ZeroRequest, StepFunction} from '0http-bun'
const {router} = http({
port: 3000,
errorHandler: (err: Error) => {
console.error('Server error:', err)
return new Response('Internal Server Error', {status: 500})
},
})
// Typed middleware
router.use((req: ZeroRequest, next: StepFunction) => {
req.ctx = {
startTime: Date.now(),
engine: 'bun',
}
return next()
})
// Typed route handlers
router.get('/:id', async (req: ZeroRequest) => {
return Response.json({
id: req.params.id,
context: req.ctx,
})
})
router.post('/users', async (req: ZeroRequest) => {
const body = await req.json()
return Response.json({created: true, data: body}, {status: 201})
})
interface IRouterConfig {
defaultRoute?: RequestHandler // Custom 404 handler
errorHandler?: (err: Error) => Response | Promise<Response> // Error handler
port?: number // Port number (for reference)
cacheSize?: number // Max entries per HTTP method in the route cache (default: 1000)
}
Note on
cacheSize: The router uses an LRU-style cache for resolved routes. When the cache exceedscacheSizeentries for a given HTTP method, the oldest entry is evicted. The default of1000is suitable for most applications. Increase it for APIs with many dynamic routes; decrease it to save memory in constrained environments.
The ZeroRequest extends the standard Request with additional properties:
type ZeroRequest = Request & {
params: Record<string, string> // URL parameters
query: Record<string, string> // Query string parameters
ctx?: Record<string, any> // Custom context (set by middleware)
}
// HTTP Methods
router.get(pattern, ...handlers)
router.post(pattern, ...handlers)
router.put(pattern, ...handlers)
router.patch(pattern, ...handlers)
router.delete(pattern, ...handlers)
router.head(pattern, ...handlers)
router.options(pattern, ...handlers)
router.connect(pattern, ...handlers)
router.trace(pattern, ...handlers)
// Generic method
router.on(method, pattern, ...handlers)
// All methods
router.all(pattern, ...handlers)
// Global middleware
router.use((req, next) => {
// Middleware logic
return next()
})
// Path-specific middleware
router.use('/api/*', (req, next) => {
// API-specific middleware
return next()
})
// Multiple middlewares
router.use(authMiddleware, loggingMiddleware, (req, next) => next())
import http, {ZeroRequest, StepFunction} from '0http-bun'
const {router} = http({
errorHandler: (err: Error) => {
return Response.json({error: err.message}, {status: 500})
},
})
// Logging middleware
router.use((req: ZeroRequest, next: StepFunction) => {
console.log(`${req.method} ${req.url}`)
return next()
})
// JSON body parser middleware for POST/PUT
router.use('/api/*', async (req: ZeroRequest, next: StepFunction) => {
if (req.method === 'POST' || req.method === 'PUT') {
try {
req.ctx = {...req.ctx, body: await req.json()}
} catch (err) {
return Response.json({error: 'Invalid JSON'}, {status: 400})
}
}
return next()
})
// Routes
router.get('/api/users', () => {
return Response.json([
{id: 1, name: 'John'},
{id: 2, name: 'Jane'},
])
})
router.get('/api/users/:id', (req: ZeroRequest) => {
const {id} = req.params
return Response.json({id: Number(id), name: 'User'})
})
router.post('/api/users', (req: ZeroRequest) => {
const userData = req.ctx?.body
return Response.json({id: Date.now(), ...userData}, {status: 201})
})
router.delete('/api/users/:id', (req: ZeroRequest) => {
const {id} = req.params
return Response.json({deleted: id})
})
// Start server
Bun.serve({
port: 3000,
fetch: router.fetch,
})
0http-bun includes a comprehensive middleware system with built-in middlewares for common use cases:
📦 Note: Starting with v1.2.2, some middleware dependencies are optional. Install only what you need:
jose(JWT),pino(Logger),prom-client(Prometheus).
// Import middleware functions from the middleware module
const {
createCORS,
createLogger,
createBodyParser,
createJWTAuth,
createRateLimit,
} = require('0http-bun/lib/middleware')
const {router} = http()
// Apply middleware stack
router.use(createCORS()) // Enable CORS
router.use(createLogger()) // Request logging
router.use(createBodyParser()) // Parse request bodies
router.use(createRateLimit({max: 100})) // Rate limiting
// Protected routes
router.use('/api/*', createJWTAuth({secret: process.env.JWT_SECRET}))
📖 Complete Middleware Documentation
The default error handler returns a generic "Internal Server Error" response (HTTP 500) and logs the full error to console.error. This prevents leaking stack traces or internal details to clients.
You can provide a custom errorHandler for more control:
import http, {ZeroRequest} from '0http-bun'
const {router} = http({
errorHandler: (err: Error) => {
console.error('Application error:', err)
// Custom error responses based on error type
if (err.name === 'ValidationError') {
return Response.json(
{error: 'Validation failed', details: err.message},
{status: 400},
)
}
return Response.json({error: 'Internal server error'}, {status: 500})
},
defaultRoute: () => {
return Response.json({error: 'Route not found'}, {status: 404})
},
})
// Errors thrown in both sync and async handlers are caught automatically
router.get('/api/risky', async (req: ZeroRequest) => {
const data = await fetchExternalData() // async errors are caught too
if (!data) {
throw new Error('Data not found')
}
return Response.json(data)
})
Async error handling: Errors thrown or rejected in async middleware/handlers are automatically caught and forwarded to the
errorHandler. You do not need to wrap every handler in try/catch.
0http-bun is designed with security-first principles and includes comprehensive protection against common web vulnerabilities. The core framework and middleware have been thoroughly penetration-tested and hardened.
.., null bytes are stripped; originalName preserved for reference)__proto__, constructor, prototype) in body parser, multipart parser, and URL-encoded parser using Object.create(null) for safe objectscrypto.timingSafeEqual()cacheSize limit (default: 1000)maxKeys eviction, and automatic cleanup intervals%2F preservation, . / .. resolution)Object.freeze({}) to prevent cross-request data leakage"Internal Server Error" without exposing stack traces or err.messageconsole.error for debugging"Invalid or expired token" regardless of the specific failure reason['HS256']; algorithm confusion preventedpath + '/') instead of prefix matchingoptional: true, invalid tokens set req.ctx.authError and req.ctx.authAttempted for downstream inspectionreq.ip || req.remoteAddress || req.socket?.remoteAddress || 'unknown' — proxy headers are not trusted by defaultreq.rateLimitStore override)maxKeys (default: 10,000) with periodic cleanupMemoryStore.increment is synchronous to eliminate TOCTOU race conditionsnull and missing origins are rejected before calling validator functions or checking arrays, preventing sandboxed iframe bypassVary: Origin is set for all non-wildcard origins to prevent CDN cache poisoning// Secure server configuration
const {router} = http({
cacheSize: 500, // Limit route cache for constrained environments
errorHandler: (err: Error) => {
// Never expose stack traces in production
return Response.json({error: 'Internal server error'}, {status: 500})
},
defaultRoute: () => {
return Response.json({error: 'Not found'}, {status: 404})
},
})
// Apply security middleware stack
router.use(
createCORS({
origin: ['https://yourdomain.com'], // Restrict origins — null origins are rejected
credentials: true,
}),
)
router.use(
createRateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
// Behind a reverse proxy? Provide a custom keyGenerator:
// keyGenerator: (req) => req.headers.get('x-forwarded-for') || req.ip || 'unknown',
}),
)
router.use(
'/api/*',
createJWTAuth({
secret: process.env.JWT_SECRET,
algorithms: ['HS256'], // Default — explicit for clarity
// For RS256, use jwksUri instead of secret:
// jwksUri: 'https://your-idp.com/.well-known/jwks.json',
// algorithms: ['RS256'],
}),
)
// Secure body parsing
router.use(
createBodyParser({
json: {limit: '10mb'},
urlencoded: {limit: '10mb'},
multipart: {limit: '50mb'},
}),
)
0http-bun provides built-in security monitoring capabilities:
// Security logging with request context
router.use(
createLogger({
serializers: {
req: (req) => ({
method: req.method,
url: req.url,
ip:
req.ip || req.remoteAddress || req.socket?.remoteAddress || 'unknown',
userAgent: req.headers.get('user-agent'),
}),
},
}),
)
// Prometheus metrics for security monitoring
router.use(
createPrometheusMetrics({
prefix: 'http_',
labels: ['method', 'route', 'status_code'],
}),
)
keyGenerator for rate limiting that reads the appropriate header (e.g., X-Forwarded-For)algorithms — do not rely on defaultsSecurity is a continuous process. While 0http-bun provides strong security foundations, always follow security best practices and conduct regular security assessments for your applications.
0http-bun is designed for high performance with Bun's native capabilities:
trouter libraryfast-querystring for optimal performancecacheSize limit, immutable shared objects, and minimal allocations. / .. resolution)Run benchmarks with:
bun run bench
Performance characteristics will vary based on your specific use case and middleware stack.
Full TypeScript support is included with comprehensive type definitions:
// Main framework types
import {
ZeroRequest,
StepFunction,
RequestHandler,
IRouter,
IRouterConfig,
} from '0http-bun'
// Middleware-specific types
import {
LoggerOptions,
JWTAuthOptions,
APIKeyAuthOptions,
RateLimitOptions,
CORSOptions,
BodyParserOptions,
MemoryStore,
} from '0http-bun/lib/middleware'
// Example typed middleware
const customMiddleware: RequestHandler = (
req: ZeroRequest,
next: StepFunction,
) => {
req.ctx = req.ctx || {}
req.ctx.timestamp = Date.now()
return next()
}
// Example typed route handler
const typedHandler = (req: ZeroRequest): Response => {
return Response.json({
params: req.params,
query: req.query,
context: req.ctx,
})
}
0http-bun is trusted by developers for production workloads thanks to its comprehensive feature set:
cacheSize for bounded memory usage0http-bun consistently outperforms other frameworks in Bun environments:
| Framework | Requests/sec | Memory Usage | Latency (p95) |
|---|---|---|---|
| 0http-bun | ~85,000 | ~45MB | ~2.1ms |
| Express | ~42,000 | ~68MB | ~4.8ms |
| Fastify | ~78,000 | ~52MB | ~2.4ms |
| Hono | ~82,000 | ~48MB | ~2.2ms |
Benchmarks run on Bun v1.2.2 with simple JSON response routes. Results may vary based on hardware and configuration.
Adversarial review of the v1.3.0 hardening pass. Remaining gaps in path handling, middleware consistency, and hot-path cost are addressed.
%2e%2e / .. / . are now resolved after URI decoding so routing and excludePaths cannot disagree (the previous pass collapsed slashes and decoded, but left .. in the path).excludePaths now use the same req.path the router computed, instead of new URL(req.url).pathname.excludePaths now uses exact-or-boundary matching (same as JWT and rate-limit). /health no longer skips /healthcheck.Vary: Origin — set for static string origins as well as function/array origins.null origin — rejected for all non-wildcard configs, including origin: 'null'.Set-Cookie, Authorization, Cookie, and Proxy-Authorization are redacted in the default serializer.req.ctx.authError is a generic message, not the raw jose error.maxKeys (default 10,000) plus amortized cleanup. When full, new keys fail closed (429) instead of evicting live counters.Symbol.for('0http.canonicalPath') or re-parses req.url; mutable req.path is ignored.new URL() on every request when req.path is set.jsonTypes parsers are cached instead of being created per request.req.path, req.body, req.files, logger options (level, requestIdHeader, generateRequestId), errorHandler(err, req), extended on body parser, and maxKeys on rate limit.ParsedFile.data is Uint8Array (matches the implementation). req.jwt.token removed from types (removed in 1.3.0).(origin, req) as documented.bun run lint (check) instead of format (write). Bench script points at bench.ts.This release addresses 43 vulnerabilities (6 Critical, 13 High, 13 Medium, 7 Low, 4 Info) identified in a comprehensive penetration test. All 43 issues have been resolved.
| Change | Old Behavior | New Behavior |
|---|---|---|
| Rate limit key generator | Trusted X-Forwarded-For, X-Real-IP, CF-Connecting-IP proxy headers | Uses req.ip || req.remoteAddress || 'unknown' only. Supply a custom keyGenerator if behind a reverse proxy. |
| JWT default algorithms | ['HS256', 'RS256'] | ['HS256'] only. Mixed symmetric + asymmetric algorithms throw an error. Explicitly set algorithms: ['RS256'] if using RSA keys. |
| Default error handler | Returned err.message to the client in the response body | Returns generic "Internal Server Error". Full error logged server-side via console.error. |
parseLimit validation | Silently returned 1MB default for false, null, objects | Throws TypeError for unexpected types. |
| JWT token in context | req.jwt.token and req.ctx.jwt.token contained the raw JWT string | Raw token no longer stored on the request context. Only payload and header are available. |
| JWT module exports | Exported internal functions (extractToken, handleAuthError, extractTokenFromHeader, etc.) | Only exports createJWTAuth, createAPIKeyAuth, and API_KEY_SYMBOL. Internal helpers are no longer exposed. |
| Rate limit store override | req.rateLimitStore could override the configured store at runtime | Always uses the constructor-configured store. req.rateLimitStore has no effect. |
| API key in context | req.apiKey / req.ctx.apiKey contained the raw API key | Now stores a masked version (xxxx****xxxx). Raw key available via req[API_KEY_SYMBOL]. |
| Empty JSON body | Empty/whitespace JSON body silently returned {} | Now sets req.body to undefined. Applications must handle undefined explicitly. |
cacheSize (default: 1000) — prevents unbounded memory growth from cache poisoning attacks.%2F), preventing route filter bypass via //admin or %2e%2e paths.Object.freeze({}) prevents cross-request data leakage on parameterless routes.router.use() return fix — return this in arrow function corrected to return router for proper chaining.__proto__, constructor, prototype) are filtered from parsed query strings, mirroring existing route params protection..catch() and forwarded to the errorHandler. Previously, they were unhandled.{"key": "{{{..."} could evade the depth check.jsonParser enforces size limits — size validation runs before the custom parser function is called.Object.create(null) for body/files objects and blocklists dangerous property names..., path separators (/, \), null bytes, and leading dots. The original filename is preserved in file.originalName.application/json only (was application/ which matched application/xml, application/octet-stream, etc.).parseLimit type validation — throws TypeError on unexpected input types instead of silently defaulting.req.body to undefined instead of silently returning {}. (Breaking change)Symbol.for('0http.rawBody') (RAW_BODY_SYMBOL) instead of req._rawBodyText, preventing accidental serialization/logging. The symbol is exported from the body parser module.crypto.timingSafeEqual() with constant-time length mismatch handling.HS*) and asymmetric (RS*/ES*/PS*) algorithms are configured.path + '/' boundary checking replaces prefix matching (prevents /healthcheck bypassing /health exclusion).optional: true and a token is invalid, sets req.ctx.authError and req.ctx.authAttempted = true instead of silently proceeding."Invalid or expired token" to prevent oracle attacks. Distinct messages are used for missing tokens and API key failures....jwtOptions spread applied first; security-critical options (algorithms, audience, issuer) override after.apiKeyValidator(apiKey, req) regardless of Function.length arity.createJWTAuth and createAPIKeyAuth are exported.requiredTokenType option validates the JWT typ header claim (case-insensitive). Rejects tokens with missing or incorrect type when configured.req[API_KEY_SYMBOL] (exported Symbol.for('0http.apiKey')). req.apiKey and req.ctx.apiKey now store a masked version (xxxx****xxxx). (Breaking change)catch blocks in handleAuthError now log errors via console.error for debugging visibility.req.ip || req.remoteAddress || 'unknown' instead of trusting proxy headers.maxKeys option (default: 10,000) with periodic cleanup via setInterval + unref().MemoryStore.increment — eliminates TOCTOU race condition in the fixed-window store.excludePaths uses exact or boundary matching.standardHeaders now accepts true (full headers, default), false (no headers), or 'minimal' (only Retry-After on 429 responses) to control rate limit information disclosure.'unknown' bucket, preventing shared-bucket DoS.null/missing origins rejected before calling validator functions or checking arrays, preventing sandboxed iframe bypass.Vary: Origin — set for all non-wildcard origins (previously only set for function/array origins).allowedHeaders function is now resolved once per preflight request instead of multiple times, preventing inconsistency.jose, pino, prom-client) made optional with lazy loading.The default keyGenerator no longer reads proxy headers. If your application runs behind a reverse proxy (nginx, Cloudflare, AWS ALB, etc.), you must provide a custom keyGenerator:
router.use(
createRateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
keyGenerator: (req) => {
// Trust the header your reverse proxy sets
return (
req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ||
req.ip ||
'unknown'
)
},
}),
)
If you were relying on the default ['HS256', 'RS256'] algorithm list, you must now be explicit:
// For HMAC (symmetric) secrets:
createJWTAuth({
secret: process.env.JWT_SECRET,
algorithms: ['HS256'], // This is now the default
})
// For RSA (asymmetric) keys:
createJWTAuth({
jwksUri: 'https://your-idp.com/.well-known/jwks.json',
algorithms: ['RS256'], // Must be explicit — mixing with HS256 will throw
})
If you relied on err.message being returned to clients from the default error handler, provide a custom errorHandler:
const {router} = http({
errorHandler: (err: Error) => {
// Old behavior (NOT recommended for production):
return new Response(err.message, {status: 500})
},
})
parseLimit ValidationIf your code passes non-string/non-number values to parseLimit (e.g., false, null, objects), update it to pass a valid value:
// Before (silently defaulted to 1MB):
createBodyParser({json: {limit: someConfig.limit}}) // someConfig.limit might be null
// After (throws TypeError):
createBodyParser({json: {limit: someConfig.limit || '1mb'}}) // Provide a fallback
If you accessed the raw JWT token string via req.jwt.token or req.ctx.jwt.token, note that only payload and header are now available:
// Before:
const rawToken = req.jwt.token // No longer available
// After:
const payload = req.jwt.payload // Decoded payload
const header = req.jwt.header // Protected header
If you accessed the raw API key via req.apiKey or req.ctx.apiKey, note that these now contain a masked version. Use the exported API_KEY_SYMBOL for programmatic access:
import {API_KEY_SYMBOL} from '0http-bun/lib/middleware/jwt-auth'
// Before:
const rawKey = req.apiKey // Was the raw API key, now masked (xxxx****xxxx)
// After:
const rawKey = req[API_KEY_SYMBOL] // Symbol.for('0http.apiKey')
const maskedKey = req.apiKey // 'xxxx****xxxx' (safe for logging)
If your code relied on empty JSON request bodies being parsed as {}, update it to handle undefined:
// Before (empty body → {}):
router.post('/api/data', (req) => {
const keys = Object.keys(req.body) // Always worked
})
// After (empty body → undefined):
router.post('/api/data', (req) => {
const body = req.body || {}
const keys = Object.keys(body) // Handle undefined
})
If you accessed raw body text via req._rawBodyText, use the exported RAW_BODY_SYMBOL instead:
import {RAW_BODY_SYMBOL} from '0http-bun/lib/middleware/body-parser'
// Before:
const rawBody = req._rawBodyText // Public string property
// After:
const rawBody = req[RAW_BODY_SYMBOL] // Symbol.for('0http.rawBody')
MIT
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
# Clone the repository
git clone https://github.com/BackendStack21/0http-bun.git
cd 0http-bun
# Install dependencies
bun install
# Run tests
bun test
# Run benchmarks
bun run bench
# Format code
bun run format
Download counts and popularity will appear here as the collector receives them.
| Package / target | Requirement | Relationship |
|---|---|---|
joseEnvironment{
"local_name": "jose",
"alias": false,
"peer_metadata": null
} | ^6.0.11 | runtime |
pinoEnvironment{
"local_name": "pino",
"alias": false,
"peer_metadata": null
} | ^9.7.0 | runtime |
trouterEnvironment{
"local_name": "trouter",
"alias": false,
"peer_metadata": null
} | ^4.0.0 | runtime |
fast-querystringEnvironment{
"local_name": "fast-querystring",
"alias": false,
"peer_metadata": null
} | ^1.1.2 | runtime |
mitataEnvironment{
"local_name": "mitata",
"alias": false,
"peer_metadata": null
} | ^1.0.34 | development |
prettierEnvironment{
"local_name": "prettier",
"alias": false,
"peer_metadata": null
} | ^3.5.3 | development |
0http-bunEnvironment{
"local_name": "0http-bun",
"alias": false,
"peer_metadata": null
} | ^1.1.2 | development |
bun-typesEnvironment{
"local_name": "bun-types",
"alias": false,
"peer_metadata": null
} | ^1.2.15 | development |
typescriptEnvironment{
"local_name": "typescript",
"alias": false,
"peer_metadata": null
} | ^5.8.3 | development |
Source distribution
{
"shasum": "52b23f60086d4e565af2f9fece11b5ad67c3a058",
"tarball": "https://registry.npmjs.org/0http-bun/-/0http-bun-1.2.1.tgz",
"fileCount": 17,
"integrity": "sha512-QXhW4RFReF+bMoETyAnovJ4oSIrsaIHuB/07zslpzcVeEJkXSlqnbDwudGAxLwStky5sgAU84ElkGOX5xiUdig==",
"signatures": [
{
"sig": "MEYCIQCZw5UOo2+hq/9j7IrYg1kIQsFtQCsjyWVpvwjXM9fNAQIhAIfJWF1mUgvextOmYTqGVkuAHyvyrmfvHAq2nnv1mS81",
"keyid": "SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"
}
],
"unpackedSize": 107256
}Observed since 20 Oct, 17:41 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package_version:bd0c4be8ab5ea5d16b42e30ea87231f99882c69c4573c39b3c3efb524cb1cab2