diff --git a/docs/README.md b/docs/README.md index dc66c90..014bc94 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,17 +5,38 @@ [![TypeScript support](https://badgen.net/npm/types/0http)](https://www.npmjs.com/package/0http) [![Github stars](https://badgen.net/github/stars/jkyberneees/0http?icon=github)](https://github.com/jkyberneees/0http) - +
+
+ 0http Logo +
+
+ Zero friction HTTP framework for Node.js +
+ Tweaked for high throughput, low overhead, and maximum flexibility. +
+
+ +## Why 0http? + +- 🚀 **Blazing Fast**: One of the fastest Node.js web frameworks. Optimized for speed with smart caching and efficient routing. +- 🛠 **Highly Configurable**: Swap routers, servers, and customize behavior to fit your needs. +- 🔌 **Middleware Support**: Express-like middleware chain with full `async/await` support. +- ʦ **TypeScript Ready**: First-class TypeScript support for type-safe development. +- 🧩 **Nested Routing**: Powerful nested router support for modular architectures, optimized for static paths. +- 🛡️ **Production Ready**: Secure defaults with environment-aware error handling. + +--- + +## Installation -Zero friction HTTP framework: -- Tweaked Node.js HTTP server for high throughput. -- High-performance and customizable request routers. +```bash +npm install 0http +``` -![Performance Benchmarks](Benchmarks.png) -> Check it yourself: https://web-frameworks-benchmark.netlify.app/result?f=feathersjs,0http,koa,fastify,nestjs-express,express,sails,nestjs-fastify,restana +## Quick Start + +### JavaScript -# Usage -JavaScript: ```js const zero = require('0http') const { router, server } = zero() @@ -25,17 +46,17 @@ router.get('/hello', (req, res) => { }) router.post('/do', (req, res) => { - // ... res.statusCode = 201 - res.end() + res.end('Done!') }) -//... - -server.listen(3000) +server.listen(3000, () => { + console.log('Server listening on port 3000') +}) ``` -TypeScript: +### TypeScript + ```ts import zero from '0http' import { Protocol } from '0http/common' @@ -43,85 +64,78 @@ import { Protocol } from '0http/common' const { router, server } = zero() router.use((req, res, next) => { + console.log('Request received') return next() }) router.get('/hi', (req, res) => { - res.end(`Hello World from TS!`) + res.end('Hello World from TS!') }) server.listen(3000) ``` -# Routers -`0http` allows you to define the router implementation you prefer as soon as it support the following interface: +--- + +## Core Capabilities + +### 1. Pluggable Routers +`0http` allows you to define the router implementation you prefer. + +#### Sequential Router (Default) +An extended implementation of [trouter](https://www.npmjs.com/package/trouter). +- **Features**: Middleware support, nested routers, regex matching. +- **Performance**: Uses an internal LRU cache (optional) to store matching results, making it extremely fast even with many routes. +- **Supported Verbs**: `GET, HEAD, PATCH, OPTIONS, CONNECT, DELETE, TRACE, POST, PUT` + ```js -router.lookup = (req, res) // -> should trigger router search and handlers execution +const sequential = require('0http/lib/router/sequential') +const { router } = zero({ + router: sequential({ + cacheSize: 2000 // Configurable cache size + }) +}) ``` -## 0http - sequential (default router) -This a `0http` extended implementation of the [trouter](https://www.npmjs.com/package/trouter) router. Includes support for middlewares, nested routers and shortcuts for routes registration. -As this is an iterative regular expression matching router, it tends to be slower than `find-my-way` when the number of registered routes increases; to mitigate this issue, we use -an internal(optional) LRU cache to store the matching results of the previous requests, resulting on a super-fast matching process. - -Supported HTTP verbs: `GET, HEAD, PATCH, OPTIONS, CONNECT, DELETE, TRACE, POST, PUT` +#### Find-My-Way Router +Integration with [find-my-way](https://github.com/delvedor/find-my-way), a super-fast Radix Tree router. +- **Best for**: Static paths and high performance without regex overhead. +- **Note**: Does not support all the middleware goodies of the sequential router. ```js -const zero = require('0http') -const { router, server } = zero({}) +const { router } = zero({ + router: require('find-my-way')() +}) +``` + +### 2. Middleware Engine +The middleware engine is optimized for performance and flexibility. -// global middleware example +#### Global & Route Middleware +```js +// Global middleware router.use('/', (req, res, next) => { - res.write('Hello ') + res.setHeader('X-Powered-By', '0http') next() }) -// route middleware example -const routeMiddleware = (req, res, next) => { - res.write('World') +// Route-specific middleware +const auth = (req, res, next) => { + if (!req.headers.authorization) { + res.statusCode = 401 + return res.end('Unauthorized') + } next() } -// GET /sayhi route with middleware and handler -router.get('/sayhi', routeMiddleware, (req, res) => { - res.end('!') +router.get('/protected', auth, (req, res) => { + res.end('Secret Data') }) - -server.listen(3000) ``` -### Configuration Options -- **defaultRoute**: Route handler when there is no router matching. Default value: - ```js - (req, res) => { - res.statusCode = 404 - res.end() - } - ``` -- **cacheSize**: The size of the LRU cache for router matching. If the value is `0`, the cache will be disabled. If the value is `<0`, the cache will have an unlimited size. If the value is `>0`, an LRU Cache will be used. Default value: `-1`, for extreme performance. -- **errorHandler**: Global error handler function. Default value: - - ```js - (err, req, res) => { - res.statusCode = 500 - res.end(err.message) - } - ``` - -* **prioRequestsProcessing**: `true` to use SetImmediate to prioritize router lookup, `false` to disable. By default `true`, if used with native Node.js `http` and `https` servers. Set to `false`, if using Node.js Native Addon server, such as uWebSockets.js, as this will cause a huge performance penalty -Example passing configuration options: +#### Async/Await Support +Fully supports async middlewares for clean code. -```js -const sequential = require('0http/lib/router/sequential') -const { router, server } = zero({ - router: sequential({ - cacheSize: 2000 - }) -}) -``` - -### Async middlewares -You can use async middlewares to await the remaining chain execution. Let's describe with a custom error handler middleware: ```js router.use('/', async (req, res, next) => { try { @@ -131,91 +145,90 @@ router.use('/', async (req, res, next) => { res.end(err.message) } }) - -router.get('/sayhi', (req, res) => { - throw new Error('Uuuups!') -}) ``` -### Nested Routers -You can simply use `sequential` router instances as nested routers: -```js -const zero = require('../index') -const { router, server } = zero({}) - -const nested = require('0http/lib/router/sequential')() -nested.get('/url', (req, res, next) => { - res.end(req.url) -}) -router.use('/v1', nested) +### 3. Nested Routers +Organize your application with modular nested routers. `0http` optimizes static nested routes for better performance. -server.listen(3000) -``` +```js +const zero = require('0http') +const { router, server } = zero() -## find-my-way router -> https://github.com/delvedor/find-my-way +const v1 = require('0http/lib/router/sequential')() -Super-fast raw HTTP router with no goodies. Internally uses a [Radix Tree](https://en.wikipedia.org/wiki/Radix_tree) -router that will bring better performance over iterative regular expressions matching. -```js -const zero = require('../index') -const { router, server } = zero({ - router: require('find-my-way')() -}) +v1.get('/users', (req, res) => res.end('User List')) +v1.get('/posts', (req, res) => res.end('Post List')) -router.on('GET', '/hi', (req, res) => { - res.end('Hello World!') -}) +// Mount the nested router +router.use('/api/v1', v1) server.listen(3000) ``` -# Servers -`0http` is just a wrapper for the servers and routers implementations you provide. +### 4. Custom Servers +`0http` is server-agnostic. You can use the standard Node.js `http.Server`, `https.Server`, or even custom implementations. + ```js +const https = require('https') +const fs = require('fs') const zero = require('0http') +const options = { + key: fs.readFileSync('key.pem'), + cert: fs.readFileSync('cert.pem') +} + const { router, server } = zero({ - server: yourCustomServerInstance + server: https.createServer(options) }) + +server.listen(443) ``` -## Node.js http.Server -If no server is provided by configuration, the standard Node.js [http.Server](https://nodejs.org/api/http.html#http_class_http_server) implementation is used. -Because this server offers the best balance between Node.js ecosystem compatibility and performance, we highly recommend it for most use cases. +--- -# Benchmarks (30/12/2019) -**Node version**: v12.14.0 -**Laptop**: MacBook Pro 2019, 2,4 GHz Intel Core i9, 32 GB 2400 MHz DDR4 -**Server**: Single instance +## Configuration Options -```bash -wrk -t8 -c40 -d5s http://127.0.0.1:3000/hi -``` +Pass a configuration object to `zero(config)`: + +| Option | Description | Default | +|--------|-------------|---------| +| `router` | Custom router instance. | `sequential()` | +| `server` | Custom server instance. | `http.createServer()` | +| `defaultRoute` | Handler for 404 Not Found. | `(req, res) => { res.statusCode = 404; res.end() }` | +| `errorHandler` | Global error handler. | Production-safe error handler (hides stack traces in prod). | +| `prioRequestsProcessing` | Use `setImmediate` to prioritize request processing. | `true` (for Node.js http/https) | + +### Sequential Router Options +| Option | Description | Default | +|--------|-------------|---------| +| `cacheSize` | LRU cache size. `0` to disable, `<0` for unlimited. | `-1` (Unlimited) | + +--- + +## Benchmarks + +![Performance Benchmarks](Benchmarks.png) + +> **Note**: Benchmarks are subject to hardware and environment. +> Check the latest independent results: [Web Frameworks Benchmark](https://web-frameworks-benchmark.netlify.app/result?f=feathersjs,0http,koa,fastify,nestjs-express,express,sails,nestjs-fastify,restana) + +**Snapshot (MacBook Pro i9, Node v12):** +- **0http (sequential)**: ~88k req/sec +- **0http (find-my-way)**: ~87k req/sec +- **restana**: ~73k req/sec + +--- + +## Ecosystem + +- **[low-http-server](https://github.com/jkyberneees/low-http-server)**: A low-level HTTP server implementation for extreme performance, originally part of 0http. + +## Support + +If you love this project, consider supporting its maintenance: +- **PayPal**: [Donate](https://www.paypal.me/kyberneees) + +## License -## 1 route registered -- 0http (sequential) - `Requests/sec: 88438.69` -- 0http (find-my-way) - `Requests/sec: 87597.44` -- restana v3.4.2 - `Requests/sec: 73455.97` - -## 5 routes registered -- **0http (sequential)** - `Requests/sec: 85839.17` -- 0http (find-my-way) - `Requests/sec: 82682.86` - -> For more accurate benchmarks please see: -> -> - https://github.com/the-benchmarker/web-frameworks - -# Support / Donate 💚 -You can support the maintenance of this project: -- PayPal: https://www.paypal.me/kyberneees -- [TRON](https://www.binance.com/en/buy-TRON) Wallet: `TJ5Bbf9v4kpptnRsePXYDvnYcYrS5Tyxus` - -# Breaking Changes: -## 3.x -- Low HTTP server implementation was moved to: https://github.com/jkyberneees/low-http-server \ No newline at end of file +MIT \ No newline at end of file diff --git a/lib/next.js b/lib/next.js index 7a9e410..ed376f0 100644 --- a/lib/next.js +++ b/lib/next.js @@ -39,7 +39,11 @@ function next (middlewares, req, res, index, routers, defaultRoute, errorHandler req.preRouterPath = req.path // Replace pattern in URL - this is a hot path, optimize it - req.url = req.url.replace(pattern, '') + if (typeof pattern === 'number') { + req.url = req.url.slice(pattern) + } else { + req.url = req.url.replace(pattern, '') + } // Ensure URL starts with a slash - use charCodeAt for performance if (req.url.length === 0 || req.url.charCodeAt(0) !== 47) { // 47 is '/' diff --git a/lib/router/sequential.js b/lib/router/sequential.js index fc38206..3a62b5a 100644 --- a/lib/router/sequential.js +++ b/lib/router/sequential.js @@ -15,8 +15,11 @@ const DEFAULT_ROUTE = (req, res) => { const DEFAULT_ERROR_HANDLER = (err, req, res) => { res.statusCode = 500 - // Note: err.message could expose sensitive information in production - res.end(err.message) + if (process.env.NODE_ENV === 'production') { + res.end('Internal Server Error') + } else { + res.end(err.message) + } } /** @@ -87,8 +90,10 @@ module.exports = (config = {}) => { if (firstMiddleware?.id) { // Cache router -> pattern relation for URL pattern replacement in nested routing // This enables efficient URL rewriting when entering nested router contexts - const { pattern } = parse(prefix, true) - routers[firstMiddleware.id] = pattern + const { pattern, keys } = parse(prefix, true) + routers[firstMiddleware.id] = keys.length === 0 && prefix.indexOf('*') === -1 // No params and no wildcards + ? prefix.length // Static match + : pattern // Regex match } return router // Ensure chainable API by returning router instance @@ -157,12 +162,7 @@ module.exports = (config = {}) => { } else if (params) { // Manual property copying - optimized for small objects // Pre-compute keys and length to avoid repeated calls - const paramKeys = Object.keys(params) - let i = paramKeys.length - while (i--) { - const key = paramKeys[i] - req.params[key] = params[key] - } + Object.assign(req.params, params) } return next(middlewares, req, res, 0, routers, defaultRoute, errorHandler) diff --git a/package.json b/package.json index fc6c91c..ef062c2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "0http", - "version": "4.3.0", + "version": "4.4.0", "description": "Zero friction HTTP request router. The need for speed!", "main": "index.js", "scripts": { @@ -52,4 +52,4 @@ "trouter": "^4.0.0" }, "types": "./index.d.ts" -} +} \ No newline at end of file diff --git a/tests/v4.4.test.js b/tests/v4.4.test.js new file mode 100644 index 0000000..abae0ec --- /dev/null +++ b/tests/v4.4.test.js @@ -0,0 +1,97 @@ +/* global describe, it, before, after */ +const cero = require('../index') +const request = require('supertest') + +describe('v4.4 Improvements', () => { + describe('Security: Default Error Handler', () => { + let originalEnv + + before(() => { + originalEnv = process.env.NODE_ENV + }) + + after(() => { + process.env.NODE_ENV = originalEnv + }) + + it('should hide error message in production', async () => { + process.env.NODE_ENV = 'production' + const { router, server } = cero() + + router.get('/error', (req, res, next) => { + next(new Error('Sensitive Info')) + }) + + await request(server) + .get('/error') + .expect(500) + .expect('Internal Server Error') + }) + + it('should show error message in development', async () => { + process.env.NODE_ENV = 'development' + const { router, server } = cero() + + router.get('/error', (req, res, next) => { + next(new Error('Sensitive Info')) + }) + + await request(server) + .get('/error') + .expect(500) + .expect('Sensitive Info') + }) + }) + + describe('Performance: Static Nested Routes', () => { + it('should handle static nested routes correctly', async () => { + const { router, server } = cero() + const nestedRouter = cero().router + + nestedRouter.get('/world', (req, res) => { + res.end('Hello World') + }) + + router.use('/hello', nestedRouter) + + await request(server) + .get('/hello/world') + .expect(200) + .expect('Hello World') + }) + + it('should handle deep static nested routes', async () => { + const { router, server } = cero() + const r1 = cero().router + const r2 = cero().router + + r2.get('/end', (req, res) => { + res.end('End') + }) + + r1.use('/level2', r2) + router.use('/level1', r1) + + await request(server) + .get('/level1/level2/end') + .expect(200) + .expect('End') + }) + + it('should still handle regex nested routes', async () => { + const { router, server } = cero() + const nestedRouter = cero().router + + nestedRouter.get('/world', (req, res) => { + res.end('Hello World') + }) + + router.use('/hello/:name', nestedRouter) + + await request(server) + .get('/hello/john/world') + .expect(200) + .expect('Hello World') + }) + }) +})