npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

topbit

v3.6.6

Published

A Server-side web framework support http/1.1 and http/2

Readme

Topbit

🪭 中文文档 ☯️

Topbit is a server-side Web framework based on Node.js. It has no third-party dependencies and is optimized for extreme performance with a unique routing and middleware grouping execution mechanism.

Core Features:

  • Request Context Design: Shields interface differences.
  • Global Middleware Pattern.
  • Route Grouping and Naming.
  • Group-Based Middleware Execution: Middleware is matched and executed based on request methods and route groups.
  • Daemon Mode: Supports multi-process clustering and automated worker process load adjustment.
  • Load Monitoring: Displays child process load status.
  • Body Parsing: Parses body data by default.
  • HTTP/1.1 & HTTP/2: Supports enabling HTTP/1.1 or HTTP/2 via configuration. Allows simultaneous support for HTTP/2 and HTTP/1.1.
  • HTTPS Support: Supports HTTPS configuration (HTTP/2 service requires HTTPS).
  • Request Limiting: Limits the maximum number of visits per single IP within a specific time period.
  • IP Blacklist and Whitelist.
  • Memory Management: In cluster mode, monitors child processes and restarts them if they exceed the maximum memory limit.
  • Auto-Scaling: Optional automatic load mode: creates new child processes to handle requests based on load and restores the initial state when idle.
  • Security: Default settings related to network security to avoid DDoS attacks and other network security issues at the software service level.

📦Installation

npm i topbit

You can also install via yarn:

yarn add topbit

Minimal Example

'use strict'

const Topbit = require('topbit')

const app = new Topbit({
  debug: true
})

//in debug mode, output service info
app.printServInfo().run(1234)

When no routes are added, Topbit adds a default route:

/*

Visiting via a browser will show a very simple page. This is merely for initial understanding and documentation access; it will not affect actual development.

Add a Route

'use strict'

const Topbit = require('topbit')

const app = new Topbit({
  debug: true
})


app.get('/', async ctx => {
  ctx.to('success')
})

// Defaults to listening on 0.0.0.0, parameters are consistent with the native listen interface.
app.printServInfo().run(1234)

ctx.data is the response data to be returned. You can also use ctx.to(data).

Actually, ctx.to() internally sets the value of ctx.data. Using ctx.to() to set return data is recommended.

Using Import

In .mjs files, you can use ES6 import:

import Topbit from 'topbit'

const app = new Topbit({
  debug: true
})

app.get('/', async ctx => {
    ctx.to('success')
})

app.printServInfo().run(1234)

Routes and Request Types

The HTTP start line defines the request type, also known as the Request Method. Current request methods:

GET POST PUT PATCH DELETE OPTIONS  TRACE HEAD

The first 6 are the most commonly used. For each request type, the router has a corresponding lowercase function for mounting routes. For convenience, after initializing the app, you can use the shortcut calls with the same names on the app instance. (The framework supports these types.)

Example:


'use strict'

const Topbit = require('topbit')

const app = new Topbit({
  debug: true
})

app.get('/', async c => {
  c.to('success')
})

app.get('/p', async c => {
  c.to(`${c.method} ${c.routepath}`)
})

app.post('/', async c => {
  // Return uploaded data
  c.to(c.body)
})

app.put('/p', async c => {
  c.to({
    method : c.method,
    body : c.body,
    query : c.query
  })
})

// Defaults to listening on 0.0.0.0
app.printServInfo().run(8080)

Get URL Parameters

  • Query strings in the URL (parameters like ?a=1&b=2) are parsed into c.query.
  • Form submitted data is parsed into c.body.

The content-type for forms is application/x-www-form-urlencoded.

'use strict';

const Topbit = require('topbit');

let app = new Topbit({
    debug: true
})

app.get('/q', async c => {
  // Query strings after ? in URL are parsed into query.
  // Returns JSON text, main difference is content-type in header is text/json
  c.to(c.query)
})

app.post('/p', async c => {
  // Data submitted via POST/PUT is saved to body. 
  // If it's a form, it is automatically parsed; otherwise, raw text value is saved.
  // Middleware can be used to handle various data types.
  c.to(c.body)
})

app.run(2019)

Get POST Data

Requests that submit body data are POST and PUT. In front-end pages, this is usually form submission or asynchronous requests.

  • Form submitted data is parsed into c.body.

Form content-type is application/x-www-form-urlencoded.

Asynchronous request data often has a content-type of application/json.

For both types above, c.body will be an object.

'use strict'

const Topbit = require('topbit')

let app = new Topbit({debug: true})

app.post('/p', async c => {
  // POST/PUT data saved to body. Forms are auto-parsed to object.
  // Middleware can be used to process various data.
  c.to(c.body)
});

app.run(2019)

About content-type

application/x-www-form-urlencoded

Basic form types are parsed into c.body as a JS object.

text/*

If content-type is text/* (starts with text/), such as text/json, the framework does not parse it. It simply converts the uploaded data to a string in utf8 format and assigns it to c.body. Subsequent processing is decided by the developer.

multipart/form-data;boundary=xxx

If content-type is a file upload type, it is parsed by default. The parsed file objects are placed in c.files, accessible via c.getFile.

application/json

This type will be parsed using JSON.parse.

Other types

If content-type is any other type, c.body defaults to point to c.rawBody, which is the rawest Buffer data.

The framework provides basic core support. Other types need to be handled by the developer or via extensions.

To be easy to use while leaving enough space for developers, you can completely discard the default body parsing by setting the initialization option parseBody to false. You can also extend upon this.

The body parsing module is essentially a middleware designed to facilitate extension and replacement.

to Function Returning Data

The to function is a wrapper for c.data; it sets the value of c.data. There are two aliases: ok and oo. You can choose freely based on the scenario.


app.get('/', async c => {
  c.to('success')
})

app.get('/randerr', async c => {
  let n = parseInt(Math.random() * 10)
  if (n >= 5) {
    c.ok('success')
  } else {
    // Return 404 status code
    /*
      Equivalent to:
        c.status(404).data = 'not found'
    */
   // You can use chain calls in versions above v22.4.6.
    c.status(404).oo('not found')
  }
})

app.run(1234)

Chain Calls

You can use chain calls for setHeader, status, and sendHeader.


app.get('/', async c => {

  c.setHeader('content-type', 'text/plain; charset=utf-8')
    .setHeader('x-server', 'nodejs server')
    .status(200)
    .to(`${Date.now()} Math.random()}`)

})

Route Parameters

app.get('/:name/:id', async c => {
  // Use : to indicate route parameters, request params are parsed into c.param
  let username = c.param.name;
  let uid = c.param.id;
  c.to(`${username} ${id}`)
})

app.run(8000)

Wildcard Path Parameters

* represents any path, but it must appear at the end of the route.


app.get('/static/*', async c => {
  // Any path represented by * is parsed into c.param.starPath
  let spath = c.param.starPath

  c.to(spath)
})

Route Lookup Rules


The route lookup process strictly controls the order of parameterized routes and routes with *, rather than matching based on addition order.

Applications developed with previous versions are unaffected; there are no compatibility issues. Stricter ordering reduces the likelihood of conflicts.

Route Lookup Strategy:

  1. Ordinary string paths.
  2. Parameterized routes (routes with fewer parameters match first).
  3. Routes with * (matched in longest-to-shortest pattern).
Example:
Existing routes: /x/y/:id  /x/y/* /x/*  /x/:key/:id

/x/y/123 matches /x/y/:id first, stops matching.

/x/y/123/345 matches /x/y/* first, stops matching.

/x/q/123 will match /x/:key/:id.

/x/a.jpg will match /x/*, other routes cannot match.

/x/static/images/a.jpg will match /x/*, other routes cannot match.


Route Format Validation

router.checkRoute(path) checks whether a route string is well-formed before registering it. It does not register anything, does not throw, and has no side effects — useful when routes are generated from a configuration file, so errors surface at startup rather than as an addPath() throw.

const r = app.router;

r.checkRoute('/api/:id')
// { ok: true }

r.checkRoute('/x/:id/*')
// { ok: false, message: '...', route: '/x/:id/*' }

When ok is false, message explains the problem and route is the normalized route string (the form in which this route would actually be registered).

What it checks:

| Item | Rule | |---|---| | Type | Must be a string | | Character set | Only letters digits - _ : * / . @, max length 1000 | | Position of * | Only allowed as the last segment | | Number of * | At most one | | : with * | Cannot appear in the same route | | Parameter name | Must not be empty, must not be __proto__ |

It does not check pattern conflicts or duplicate route names. Those depend on what is already registered in the route table — runtime state rather than a property of the string itself, so the same route string yields different answers in different route tables. addPath() still checks them at registration time.

addPath() calls checkRoute() internally, so the two always agree.

Route strings are also normalized at registration: consecutive slashes are collapsed, a missing leading slash is added, and a trailing slash is stripped. router.normalizeRoute(path) returns that normalized form — //api//v2// and /api/v2 are the same route.


Grouping Routes

You can use app.middleware to specify middleware and use the returned group method to add grouped routes, or use app.group directly.

topbit.prototype.middleware(mids, options=null)

  • mids is an array. Each element is a middleware function or an array where the first element is the middleware and the second is the options for adding that middleware.
  • options defaults to null. Pass an object for options applying to all mids, e.g., {pre: true}.

topbit.prototype.group(group_name, callback, prefix=true)

  • group_name is a string representing the route group name. If it is a valid path, it also serves as the route prefix.
  • callback is a callback function. The parameters received by the callback can still call middleware and group, as well as get, post, etc., to add routes.
  • prefix is a boolean, defaults to true. It controls whether group_name is added as a route prefix. However, it only acts as a prefix if group_name is a valid route string.
'use strict'

const Topbit = require('topbit')

const app = new Topbit({
  debug: true
})

// Middleware function
let mid_timing = async (c, next) => {
  console.time('request')
  await next(c)
  console.timeEnd('request')
}

// The return value of group can use 'use' and 'pre' to add middleware.
// /api is also added to the route prefix.
app.group('/api', route => {
  route.get('/test', async c => {
    c.to('api test')
  })

  route.get('/:name', async c => {
    c.to(c.param)
  })
})

// Add middleware to the corresponding group
app.use(
  async (c, next) => {
    console.log(c.method, c.headers)
    await next(c)
  }, {group: '/sub'}
).group('/sub', route => {
  route.get('/:id', async c => {
    c.to(c.param.id)
  })
})

// Test: Does not conform to route rules, so it won't be a path prefix.
app.group('Test', route => {
  route.get('/test', async c => {
    console.log(c.group, c.name)
    c.to('test ok')
  }, 'test')
})

app.run(1234)

Specifying multiple middlewares in this way can be somewhat complex; you can use the middleware method. See the example below.

Assigning Middleware to Groups and Subgroups

'use strict'

const Topbit = require('topbit')
// Import ToFile extension
const {ToFile} = Topbit.extensions

const app = new Topbit({
  debug: true
})

// Middleware function
let mid_timing = async (c, next) => {
  console.time('request')
  await next(c)
  console.timeEnd('request')
}

let sub_mid_test = async (c, next) => {
  console.log('mid test start')
  await next(c)
  console.log('mid test end')
}

// group return value can use use, pre, middleware to add middleware.
// /api is also added to the route prefix.

app.middleware([
     // Time recording middleware, runs before receiving body data, so set pre: true
     [ mid_timing, {pre: true} ],

     // ToFile extension runs after receiving body data, only for POST and PUT requests
     [ new ToFile(), {method: ['POST', 'PUT']} ]
  ])
  .group('/api', route => {
      route.get('/test', async c => {
        c.to('api test')
      })

      route.get('/:name', async c => {
        c.to(c.param)
      })

      // Subgroup /sub enables middleware sub_mid_test.
      // Simultaneously, subgroups inherit all middleware from the upper layer.
      route.middleware([sub_mid_test])
        .group('/sub', sub => {
            sub.get('/:key', async c => {
              c.to(c.param)
            })
        })
  })

app.run(1234)

Groups support nested calls, but the hierarchy cannot exceed 9 levels. Usually, nesting more than 3 levels indicates a design issue and should be reconsidered.

This feature is not as convenient and easy to use as the automatic loading mechanism of the TopbitLoader extension. However, in practice, requirements vary. Sometimes you have to use a single file for the service while still taking advantage of the framework's routing and middleware grouping, and also conveniently writing logic-clear, structured code. Therefore, middleware and group interfaces are convenient for handling this. Also, if you are not used to TopbitLoader's MCM pattern (Middleware - Controller - Model, similar to MVC), this method combines well with other module code.

The function of assigning routing groups above is non-intrusive; it will not affect existing code, nor will it conflict with TopbitLoader.

!! Complex route handling functions should be placed in separate modules and completed using a unified automated loading function.

It supports adding via return values, so passing a callback function is not mandatory:

'use strict'

const Topbit = require('topbit')
// Import ToFile extension
const {ToFile} = Topbit.extensions

const app = new Topbit({
  debug: true
})

// Middleware function
let mid_timing = async (c, next) => {
  console.time('request')
  await next(c)
  console.timeEnd('request')
}

let sub_mid_test = async (c, next) => {
  console.log('mid test start')
  await next(c)
  console.log('mid test end')
}

let route = app.middleware([
                // Time recording middleware, pre set to true
                [ mid_timing, {pre: true} ],

                // ToFile extension runs after receiving body data, only for POST and PUT
                [ new ToFile(), {method: ['POST', 'PUT']} ]
              ])
              .group('/api')

route.get('/test', async c => {
  c.to('api test')
})

route.get('/:name', async c => {
  c.to(c.param)
})

// Subgroup /sub enables middleware sub_mid_test.
// Subgroup inherits upper layer middleware.
route.middleware([sub_mid_test])
  .group('/sub', sub => {
      sub.get('/:key', async c => {
        c.to(c.param)
      })
  })

app.run(1234)

Uploading Files

Uploaded files are parsed by default. You can turn this off by passing the parseBody option when initializing the service (detailed in options below). Parsed file data is stored in c.files. The specific structure is shown later.

'use strict'

const Topbit = require('topbit')

const app = new Topbit()

app.post('/upload', async c => {
  
  let f = c.getFile('image')

  // Generate a unique filename based on original extension + timestamp + random number.
  let fname = c.ext.makeName(f.filename)

  try {
    c.to(await c.moveFile(f, fname))
  } catch (err) {
    c.status(500).to(err.message)
  }
  
}, 'upload-image'); // Name the route 'upload-image', accessible in c.name.

app.run(1234)

c.files Data Structure

This structure is designed based on the data construction of HTTP protocol file uploads. HTTP protocol allows multiple files for the same upload name, so it parses into an array. Using getFile returns the first file by default, as usually, one upload name corresponds to one file.

For the front-end, the upload name is the name attribute in the HTML form: <input type="file" name="image">. image is the upload name; do not confuse upload name with filename.

{
  image : [
    {
      type: CONTENT_TYPE,
      filename: ORIGIN_FILENAME,
      start : START,
      end   : END,
      length: LENGTH,
      rawHeader: HEADER_DATA,
      headers: {...}
    },
    ...
  ],

  video : [
    {
      type: CONTENT_TYPE,
      filename: ORIGIN_FILENAME,
      start : START,
      end   : END,
      length: LENGTH,
      rawHeader: HEADER_DATA,
      headers: {...}
    },
    ...
  ]
}

c.getFile indexes by name. The default index value is 0. If a number less than 0 is passed, it gets the entire file array; returns null if not found.

Max Body Data Limit

'use strict'

const Topbit = require('topbit')

const app = new Topbit({
  // Allows POST or PUT request data max value to be approx 20MB.
  // Unit is bytes.
  maxBody: 20000000
})

//...

app.run(1234)

Middleware

Middleware is a very useful pattern. Implementation varies slightly between languages, but the essence is the same. The middleware mechanism allows developers to organize code better and implement complex logic easily. In fact, the entire framework runs on a middleware pattern.

Middleware Diagram:

This framework's middleware is designed to distinguish by route groups and recognize different request types to determine whether to execute or skip to the next layer. This makes it extremely fast. Multiple routes and groups have their own middleware, do not conflict, and avoid meaningless calls. Reference format:


/*
  The second parameter is optional; omitting it enables the middleware globally.
  Here, the second parameter indicates: execute only for POST requests AND the route group must be /api.
  This design ensures execution on demand, avoiding unnecessary operations.
*/
app.add(async (c, next) => {
    console.log('before');
    await next(c);
    console.log('after');
}, {method: 'POST', group: '/api'});

Middleware added with add executes in reverse order of addition (LIFO), which is the standard onion model. To provide logic that is easier to understand, the use interface is provided. Middleware added with use executes in the order of addition (FIFO). Different frameworks have different logic for order implementation, but sequential execution suits developer habits better.

It is recommended to use use to add middleware:

// Executes first
app.use(async (c, next) => {
  let start_time = Date.now()
  await next(c)
  let end_time = Date.now()
  console.log(end_time - start_time)
})

// Executes later
app.use(async (c, next) => {
  console.log(c.method, c.path)
  await next(c)
})

// use can be cascaded: app.use(m1).use(m2)

Topbit Complete Middleware Execution Model

It is important to know that internally, body data reception and parsing are also middleware. The order is deliberately arranged, separating pre and use interfaces.

Middleware Parameters

Using use or pre interfaces to add middleware supports a second parameter for precise control via option properties:

  • group: Route group. Indicates which group to execute for.
  • method: Request method. String or Array, must be uppercase.
  • name: Request name. Indicates execution only for this request name.

Example:


app.get('/xyz', async c => {
  //...
  // Route group named proxy
}, {group: 'proxy'})

app.use(proxy, {
  method : ['PUT', 'POST', 'GET', 'DELETE', 'OPTIONS'],
  // Execute for requests in route group 'proxy'.
  group : 'proxy'
})

pre (Before Receiving Body Data)

The main difference between middleware added via the pre interface and use is that pre executes before receiving body data. It can be used for permission filtering operations before receiving data. Its parameters are consistent with use.

For a consistent development experience, you can use the use interface and simply specify pre in the options:

let setbodysize = async (c, next) => {
    // Set max body receive data to ~10k.
    c.maxBody = 10000;
    await next(c);
};

// Equivalent to app.pre(setbodysize);
app.use(setbodysize, {pre: true});

Using pre allows for more complex processing, including intercepting and not executing the next layer. For example, the proxy extension uses this feature to implement a high-performance proxy service directly as a framework middleware. Its main operation is to set the request's data event to receive data at this layer, handle other logic, and return directly.

Dynamically limiting request body size based on different request types

This requirement can be solved by adding middleware via pre:


const app = new Topbit({
  // Default max body limit ~10M.
  maxBody: 10000000
})

app.pre(async (c, next) => {

  let ctype = c.headers['content-type'] || ''

  if (ctype.indexOf('text/') === 0) {
    // 50K
    c.maxBody = 50000
  } else if (ctype.indexOf('application/') === 0) {
    // 100K
    c.maxBody = 100000
  } else if (ctype.indexOf('multipart/form-data') < 0) {
    // 10K
    c.maxBody = 10000
  }

  await next(c)

}, {method: ['POST', 'PUT']})

If these parameters appear in the file simultaneously, it can look complex and be hard to maintain, but the functionality is powerful. Therefore, leaving it to automated program completion can greatly simplify coding.

For complete project structure setup, please use topbit-loader. This extension completes automatic loading of routes/models and automatic orchestration of middleware. topbit-loader

HTTPS

'use strict'

const Topbit = require('topbit')

// Just pass the path to the certificate and key files
const app = new Topbit({
    // './xxx.pem' file also works
    cert: './xxx.cert',
    key: './xxx.key'
})

app.run(1234)

Simultaneous Support for HTTP/2 and HTTP/1.1 (Compatibility Mode)

Compatibility mode utilizes the ALPN protocol and requires HTTPS, so certificates and keys must be configured.

'use strict'

const Topbit = require('topbit')

// Just pass the path to the certificate and key files
const app = new Topbit({
    cert: './xxx.cert',
    key: './xxx.key',
    // Enable http2 and allow http1; compatibility mode is enabled automatically
    http2: true,
    allowHTTP1: true
})

app.run(1234)

Configuration Options

Full configuration options for app initialization are as follows. Please read the comments carefully.

  {
    // This config represents max bytes for POST/PUT form submission and file uploads.
    maxBody   : 8000000,

    // Max number of files to parse
    maxFiles      : 12,

    /*
      After enabling daemon mode, if path is not empty string, pid is written to this file. Used for service management.
    */
    pidFile       : '',

    // Whether to enable global logging. true enables it, outputting request info or writing to file.
    globalLog: false,

    // Log output type: 'stdio' for terminal, 'file' for file output.
    logType : 'stdio',

    // File path for successful request logs
    logFile : '',

    // File path for error request logs
    errorLogFile : '',

    // Max lines per log file
    logMaxLines: 50000,

    // Max number of historical log files
    logHistory: 50,

    // Custom log handler function
    logHandle: null,

    // Enable HTTPS
    https       : false,

    http2   : false,

    allowHTTP1: false,

    // HTTPS key and cert file paths. If paths are set, https is automatically set to true.
    key   : '',
    cert  : '',

    // Server options written in 'server', passed during http service initialization. Reference http2.createSecureServer, tls.createServer
    server : {
      handshakeTimeout: 8192, // TLS handshake timeout
      //sessionTimeout: 350,
    },

    // Set server timeout in ms. Specific request timeouts can be set in specific requests.
    timeout   : 15000,

    debug     : false,

    // Enable request limiting
    useLimit: false,

    // Max connections, 0 means no limit
    maxConn : 1024,

    // Max connections per single IP within unit time, 0 means no limit
    maxIPRequest: 0,

    // Rate limit unit time for IP, 1 means 1 second. Default 60 seconds. Range 0.1 ~ 86400.
    unitTime : 60,
    
    // Display load info, requires enabling cluster mode via daemon interface
    loadMonitor : true,

    // Load info type: text, null, obj, orgobj
    // json type is for program communication, convenient for interface development
    loadInfoType : 'text',

    // Load info file path. If not set, outputs to terminal; otherwise saves to file.
    loadInfoFile : '',

    // Data to return for 404
    notFound: 'Not Found',
    
    // Data to return for 400
    badRequest : 'Bad Request',

    // Percentage parameter controlling max memory usage for child processes. Range -0.42 ~ 0.36. Base is 0.52, so default is 80%.
    memFactor: 0.28,

    // Max URL length
    maxUrlLength: 2048,

    // Timer milliseconds for child process resource reporting.
    monitorTimeSlice: 640,

    // When globalLog is true, record real IP address? Mainly used in reverse proxy mode.
    realIP: false,

    // Max allowed querystring parameters.
    maxQuery: 25,

    // Enable strong mode? If enabled, processes rejectionHandled and uncaughtException events,
    // and captures errors: TypeError,ReferenceError,RangeError,AssertionError,URIError,Error.
    strong: false,

    // Fast querystring parsing. Multiple values with same name only set the first one, not parsed as array.
    fastParseQuery: false,
    
    // Whether to auto decode Query parameters using decodeURIComponent.
    autoDecodeQuery: true,

    // In multipart format, limit max length of a single form item.
    maxFormLength: 1000000,

    /* Error handler function. Unifies collection of runtime errors:
          tlsClientError, server error, secureConnection error, clientError, thrown runtime errors.
      errname is a string marking error info and location, format --ERR-CONNECTION--, --ERR-CLIENT--, etc.

      Usually Node.js thrown errors have code and message. 
      errname is optional but passed.
      Pass custom function via config option to implement custom error collection/handling.
    */
    errorHandle: (err, errname) => {
      this.config.debug && console.error(errname, err)
    },

    // Max load rate percentage. Default 0.75 means if CPU usage > 75%, automatically create child process.
    // Must enable auto load mode via autoWorker.
    maxLoadRate: 0.75,

    // http2 protocol http2Stream timeout. If not set, -1 means consistent with timeout.
    streamTimeout: -1,

    // Request timeout. This is total request time, mainly to counter malicious requests.
    // e.g. Slowloris attacks where 1 byte is sent per second.
    // Idle timeout won't work, allowing long-term resource occupation.
    // This is a DDoS attack.
    requestTimeout: 100000,

  };
  // For HTTP status codes, only these two are needed here. Others don't strictly need full support, 
  // and you can handle them in your application implementation.
  // Because once execution starts, corresponding status codes can be returned via runtime state.
  // Before that, the framework prepares for the onion model execution, which is very fast.

Request Context

Request context is an object encapsulating various request data. This design handles differences between HTTP/1.1 and HTTP/2 protocols and incompatibilities from Node.js evolution. For design and performance, the HTTP2 module encapsulates the request object as a stream, not the http module's IncomingMessage and ServerResponse.

Request Context Properties and Description

| Property | Description | | ---- | ---- | | version | Protocol version, string type, '1.1' or '2'. | | major | Major protocol version number, 1, 2, 3 (HTTP/1.1, HTTP/2, HTTP/3). | | maxBody | Max supported request body bytes. Number. Defaults to maxBody init option. Can be set automatically in middleware based on request. | | method | Request type (GET, POST, etc.). Uppercase string. | | host | Service hostname, value of request.headers.host. | | protocol | Protocol string without colon, 'https', 'http'. | | path | Specific request path. | | routepath | Actual executed route string. | | query | URL passed parameters. | | param | Route parameters. | | files | Uploaded file information. | | body | Body request data. Format depends on content-type (string, object, buffer). | | port | Client request port number. | | ip | Client request IP address (socket address). Check x-real-ip or x-forwarded-for if using proxy. | | headers | Points to request.headers. | | isUpload() | Is it a file upload request? Checks if content-type header is multipart/form-data. | | name | Route name, defaults to empty string. | | group | Route group, defaults to empty string. | | res | HTTP/1.1 points to response, HTTP/2 points to stream. | | req | HTTP/1.1 is IncomingMessage object, HTTP/2 points to stream object. | | box | Defaults to empty object. Can add any property value to dynamically pass info to next layer components. | | service | Object for dependency injection, points to app.service. | | data | Saves final data to return to client. Assign to data or use ctx.to. Pre-v24.x was ctx.res.body. | | ext | Provides helper functions. See wiki. | | to(data) | Function to set ctx.data. | | write(data) | Directly write data to client. | | moveFile(file:object, target_filepath:string) | Function to move uploaded file to specified path. | | status() | Function to set status code. | | setHeader(k, v) | Function to set header. | | removeHeader(k) | Function to remove header waiting to be sent. | | getFile(name) | Function to get uploaded file info (reads files property). | | sendHeader() | Function to send headers for http2. setHeader only caches headers. For http/1.1, this is an empty function for code consistency. | | user | Standard property for user login, defaults to null. | | json(data) | Function, sets return data and marks type as json. | | text(data) | Function, sets return data and marks type as text. | | html(data) | Function, sets return data and marks type as html. | | pipe(filepath) | Function, stream response data. Example: await ctx.setHeader('content-type', 'text/html').pipe('./index.html') | | pipeJson(filepath) | Stream file data as json type. | | pipeText(filepath) | Stream file data as text type. | | pipeHtml(filepath) | Stream file data as html type. |

Note: to function only sets ctx.data value; data is returned at the end. It's same as direct assignment, but function calls reveal errors faster (wrong assignment adds a property without error but returns incorrect data).

Dependency Injection

The request context has a service item pointing to app.service. After initializing the app, all data/instances needed can be mounted to app.service.


'use strict';

const Topbit = require('topbit');

let app = new Topbit({
  debug: true
});

// Overwrites if exists, adds if not.
app.addService('name', 'first');
app.addService('data', {
  id : 123,
  ip : '127.0.0.1'
});

/*
This might seem useless in a single file where variables are accessible.
However, it becomes very important for module separation.
*/
app.get('/info', async c => {
  c.to({
    name : c.service.name,
    data : c.service.data
  })
})

app.run(1234)

Extending Request Context

To add extension support to the request context object, use app.httpServ.context. This property is the constructor for the request context.

Example:

'use strict'

const Topbit = require('topbit')

const app = new Topbit({
    debug: true
})

// 'this' represents the request context
app.httpServ.context.prototype.testCtx = function () {
    console.log(this.method, this.path)
}

app.get('/test', async ctx => {
    ctx.testCtx()
})

app.run(1234)

app.isMaster and app.isWorker

Node.js v16.x recommends isPrimary over isMaster. However, isMaster is still available. After Topbit initialization, app has two getters: isMaster and isWorker. They function identically to cluster properties. Purposes:

  • Avoid writing const cluster = require('cluster') again.
  • Shield future incompatible changes in cluster, enhancing code compatibility.

daemon and run

run parameters: port, host. host defaults to 0.0.0.0. Can also be sockPath (path to .sock file), supported by http listen interface. Using .sock ignores host.

daemon first two parameters match run. Third parameter is a number indicating how many child processes to use. Defaults to 0, which automatically creates child processes based on CPU core count. It maintains stable child process count, creating new ones if any terminate unexpectedly.

In cluster mode, max child processes will not exceed 2x CPU cores.

Example:


// Host defaults to 0.0.0.0, port 1234
app.run(1234)

// Listen on localhost, only accessible locally
app.run(1234, 'localhost')

// Use 2 child processes, host defaults to 0.0.0.0
app.daemon(1234, 2)

// Use 3 child processes
app.daemon(1234, 'localhost', 3)

Logging

The framework provides global logging. When using cluster mode (daemon interface), use initialization option globalLog to enable global logs and specify a log file. In single process mode, logs output to terminal (can use output/error redirection to save to file).

Note: Only daemon execution (cluster mode) allows saving logs to file natively. run (single process) outputs to screen; use IO redirection to save.

Besides file saving and terminal output, logHandle option allows custom log processing functions.

Setting logHandle invalidates logFile and errorLogFile. See code for details.

Example:


const Topbit = require('topbit')

const app = new Topbit({
  debug: true,
  // Enable global log
  globalLog: true,

  // Output to file. Default is 'stdio' (terminal).
  logType: 'file'

  // Log file for status codes 2xx or 3xx
  logFile : '/tmp/topbit.log',

  // Log file for errors (4xx or 5xx)
  errorLogFile: '/tmp/topbit-error.log',

  // Custom handler function. logFile/errorLogFile become invalid.
  // Parameters: (worker, message)
  // worker: see cluster worker docs
  /*
    msg is log object, properties:
      {
        type : '_log',
        success : true,
        log : '@ GET | https://localhost:2021/randst | 200 | 2020-10-31 20:27:7 | 127.0.0.1 | User-Agent'
      }
  */
  logHandle : (w, msg) => {
    console.log(w.id, msg)
  }

})

app.daemon(1234, 3)

Using middleware for logging does not conflict with global logging. However, middleware logging cannot capture 404s returned by the framework when no route matches (as context is not created to avoid overhead).

Furthermore, this method integrates better with cluster mode as it uses master-worker communication internally.

Message Event Handling

Based on the message event, in daemon mode (based on cluster module), a setMsgEvent function is provided to handle events sent by child processes.

This requires the message sent by worker processes to be an object, with a mandatory type property indicating the event name. Other fields can be custom.

Usage:


const Topbit = require('topbit')
const cluster = require('cluster')

const app = new Topbit({
  debug: true,
  loadInfoFile: '/tmp/loadinfo.log'
})

if (cluster.isMaster) {
  app.setMsgEvent('test-msg', (worker, msg, handle) => {
    // Child process receives message via message event
    worker.send({
      id : worker.id,
      data : 'ok'
    })

    console.log(msg)
  })
} else {
  // Receive message sent by worker.to
  process.on('message', msg => {
    console.log(msg)
  })

  setIneterval(() => {
    process.send({
      type : 'test-msg',
      pid : process.pid,
      time : (new Date()).toLocaleString()
    })
  }, 1000)

}

Sending messages from worker processes is complex. Since version 22.4.0, a send method is provided for quick message sending. It only sends to master from a worker process, so no extra worker check is needed.

app.send and app.workerMsg

Let's rewrite the worker message sending part of the code above:


const Topbit = require('topbit')

const app = new Topbit({
  debug: true,
  loadInfoFile: '/tmp/loadinfo.log'
})

// Master process registers message event type. Worker process ignores this.
app.setMsgEvent('test-msg', (worker, msg, handle) => {
  // Child process receives message via message event
  worker.send({
    id : worker.id,
    data : 'ok'
  })

  console.log(msg)
})

// Only worker process listens.
app.workerMsg(msg => {
  console.log(msg)
})

cluster.isWorker
    &&
setInterval(() => {
  // Only worker executes.
  app.send('test-msg', {
    pid: process.pid,
    time: (new Date).toLocaleString()
  })

}, 1000)

app.daemon(1234, 2)

Automatically Adjusting Child Process Quantity

The parameter passed to daemon sets the base number of child processes, e.g.:


// Use 2 child processes to handle requests.
app.daemon(1234, 2)

To automatically create child processes based on load and terminate them when idle (maintaining base quantity), use autoWorker to set a maximum value. This value must be larger than the base quantity to take effect.


// Max 9 child processes.
app.autoWorker(9)

//...

app.daemon(1234, 2)

When load is high, child processes are created. After a period of idleness, processes with 0 connections are terminated, restoring the base number.

Available in v21.9.6+. Please use the latest version as this feature has been improved for stability and performance in subsequent versions.


🛡️Strong Mode

Enable strong mode via the strong option. This monitors uncaughtException and unhandledRejection events to ensure stable operation. Simplest usage: set strong to true.

All strong mode features can be implemented via the process module; this just simplifies it.

'use strict';

const Topbit = require('topbit');

setTimeout(() => {
  // Throw exception inside timer loop
  throw new Error(`test error`)
}, 2000);

const app = new Topbit({
    // Debug mode, output errors.
    debug: true,
    // Enable strong mode
    strong: true
});

app.run(1234);

By default, strong mode captures:

'TypeError', 'ReferenceError', 'RangeError', 'AssertionError', 'URIError', 'Error'

You can customize handling by passing an object to strong.


// Core code example
const app = new Topbit({
    // Debug mode, output errors.
    debug: true,
    // Enable strong mode
    strong: {
      // Silent behavior, no error output.
      quiet: true,
      // Custom error handler
      errorHandle: (err, errname) => {
        //....
      },

      // Which errors to catch
      catchErrors: [
        'TypeError', 'URIError', 'Error', 'RangeError'
      ]

    }
})

🔔Simultaneous HTTP and HTTPS?

Note the question mark. You shouldn't do this in production. If HTTPS is enabled, HTTP isn't needed, and some frontend features won't work without HTTPS.

If needed (perhaps for testing), do this:

'use strict'

const Topbit = require('topbit')
const http = require('node:http')
const https = require('https')

const app = new Topbit({
    // Enable debug
    debug: true,
})

// Below are http/1.1 services. For http2, enable http2 and compatibility mode.
// Or use topbit-httpc extension.

// In this case, you must set event listeners manually.

let http_server = http.createServer(app.httpServ.onRequest())
let https_server = https.createServer(app.httpServ.onRequest())

http_server.listen(2025)
https_server.listen(2026)

Note: You cannot support http2 in this scenario, but you can use http2 to be compatible with http1.

Request Limiting

The framework provides IP-based rate limiting to prevent dense requests from the same IP. For HTTP/2, use http2limit module from topbit-toolkit.

'use strict';

const Topbit = require('topbit')

const app = new Topbit({
    debug : true,
    // Enable request limiting
    useLimit: true,

    // IPs allowed without frequency limit
    allow: new Set(['127.0.0.1']),

    // IPs to deny
    deny: (ip) => {
      // Example only, supports function and Set
      if (ip.indexOf('1.') === 0) return false
      return true
    },
    
    // Max requests per IP within unit time
    maxIPRequest: 6,
    
    // Unit time, 15 seconds
    unitTime: 15,
    
    // Max concurrent connections per worker process
    maxConn: 2000,

    loadMonitor: true,
    loadInfoType : 'text',
    globalLog : true,
    logType: 'stdio',
    // Load info in memory
    loadInfoFile : '--mem'
})


app.get('/', async ctx => {
  ctx.to('ok')
})

// Use 3 worker processes. Each allows 6 requests/unit time.
// Total approx 6 requests? Set maxIPRequest to 2.
app.daemon(1234, '0.0.0.0', 3)

Other

  • After running, Topbit has a final wrapper middleware. Setting c.data returns data. It detects simple text types and sets content-type automatically (text/plain, text/html, application/json) if not set.

  • Defaults to limiting max URL length and sets a max memory usage rate based on hardware.

  • All of this can be extended/overridden via config options or middleware.

  • It is fast, and we focus on optimization. If comparing, add multiple middleware and hundreds of routes to test.

  • Provides a sched function to quickly set cluster scheduling policy ('rr' or 'none'). Sets cluster.schedulingPolicy.

The framework automatically detects memory size and sets limits on initialization. You can change limits via properties in secure after init, but this requires using daemon (master managing workers).

'use strict'

const Topbit = require('topbit');

let app = new Topbit();

/*
 Operations below can be controlled via memFactor option. See config options above.
 */

// Max memory 600M, auto restart only when connection count is 0.
app.secure.maxmem = 600_000_000;

// Mandatory restart max memory limit 900M. Total memory usage including Buffers.
// Must be larger than maxmem. If memory > maxmem AND connections != 0, 
// AND continues to exceed diemem, process restarts immediately.
app.secure.diemem = 900_000_000;

// Max RSS memory 800M. Program memory usage, excluding Buffer allocations.
app.secure.maxrss = 800_000_000;

app.get('/', async c => {
  c.to('ok');
})

app.daemon(8008, 2);

Note: Requires loadMonitor option (enabled by default).

Here is the English translation of the document:


🧩 Middleware Extensions and Other Extensions 📜


As a Web framework, providing only basic functionality is not enough. Many basic functions are used across all Web services, which inevitably leads to the creation of universal extension modules.

In Topbit's global middleware design mechanism, most web service-related extensions can be completed by developing a middleware extension. You can refer to the Middleware section; this plugin mechanism is extremely flexible.

Topbit provides several universal middleware extensions that developers can use as needed.

It also provides other extension modules: Route Loader, Token Session Verification, Command Argument Parsing, Automatic Error Log Processing, and ZIP compression/decompression.

Importing Middleware Extensions

All middleware extensions are located under Topbit.extensions. Example:

'use strict'

process.chdir(__dirname)

const Topbit = require('Topbit')

// Import extensions via destructuring assignment
const {Cors, ToFile} = Topbit.extensions

const app = new Topbit({
  debug: true,
  // Enable logging, alias for globalLog
  log: true
})

// CORS extension runs before body parsing
// Middleware added via 'use' runs after body data parsing
app.pre(new Cors())
  .use(new ToFile, {method: ['PUT', 'POST']})

app.post('/file', async ctx => {
  let f = ctx.getFile('file')
  if (!f) return ctx.status(400).to('file not found')

  ctx.to(
    await f.toFile('./uploads')
  )
})

app.run(1235)

☲ Middleware Extensions

| Middleware | Description | | :--- | :--- | | Cors | Middleware to support Cross-Origin Resource Sharing (CORS). | | Resource | Static resource processing; a powerful service for serving static files. | | ToFile | Equips uploaded file objects with a toFile method. | | SSE | SSE (Server-Sent Events) protocol middleware for HTTP-based data streaming. | | Proxy | Reverse proxy for the HTTP/1.1 protocol, supporting load balancing and alive checks. | | Http2Proxy | Reverse proxy for the HTTP/2 protocol, supporting load balancing and alive checks. | | SNI | Support for multi-domain HTTPS (Server Name Indication). | | ParamCheck | Request parameter validation, supporting checks for param, query, and body. | | JWT | Token verification using the JWT protocol. (Note: Topbit.Token is recommended instead as it is much stronger than JWT). | | Cookie | Parses cookies from the frontend request headers. | | Session | Session management using a local file mechanism. |


1. Cors (Cross-Origin Support)

Middleware that supports cross-origin requests. It supports cross-origin communication via the origin header as well as resource inclusion via referer, allowing for flexible selection and configuration.

Basic Usage:

const Topbit = require('topbit')
const {Cors} = Topbit.extensions

let cr = new Cors({
    // Defaults to *, meaning cross-origin is enabled for all.
    // If you want to specify supported domains, pass an array.
    // Note: This specifies the domain of the application page accessing the API.
    allow : [
        'https://a.com',
        'https://www.a.com',
    ],

    // Defaults to content-type only
    allowHeaders : 'authorization,content-type',

    // Caches OPTIONS requests for 60 seconds; the browser will read from cache during this period.
    optionsCache: 60,

    // Defaults to '*', sets allowed request methods.
    // requestHeaders : '*'

    // Defaults to '', indicating which headers can be exposed to the requester. Multiple headers are separated by commas.
    // exposeHeaders: 'x-test-key,x-type'
})

app.pre(cr)

// Supports OPTIONS requests. Browsers send an OPTIONS preflight request before processing POST/PUT/DELETE.
// You must handle OPTIONS requests to fully support CORS.
app.options('/*', async c => {})

Advanced Configuration (Referer Check & Grouping):

let cr = new cors({
  // Do not allow empty referer
  allowEmptyReferer: false,

  // Route groups that allow empty referer.
  // Group names are custom; refer to the framework's route grouping feature.
  // This is mainly used when API development also needs to serve pages.
  emptyRefererGroup: [
    '@webapp', '@pages'
  ],

  allow: [
     // Default string format allows both origin cross-domain and referer external linking
    'https://w.a.com',
    'https://w.x.cn',

    {url: 'https://servicewechat.com/wx234hiohr23', referer: true},
    // Not applied to referer check; if a request submits referer as self-define, it will not pass
    {url: 'self-define', referer: false}
  ],
})

2. Resource (Static Resource Processing)

Static resource processing service, mainly used for hosting JS, CSS, images, audio, and other static files. Supports cache control, route prefix correction, and more.

Basic Usage:

const Topbit = require('topbit')
const {Resource} = Topbit.extensions

let st = new Resource({
    // Set the directory where static resources are located
    staticPath: './public'
})

// Execute middleware only for the 'static' group.
app.use(st, {group: 'static'})

// Add static resource route. Assuming 'css/a.css' exists in the 'public' directory.
// Access via /static/css/a.css
app.get('/static/*', async c => {
    // Request is in group 'static'
}, '@static')

Quick Example & Configuration Details:

let st = new Resource({
    // Set the directory where static resources are located
    staticPath: './public',

    // Route path: Frontend must start with /static/, mapping to files in ./public
    routePath : '/static/*',

    // Specify route group
    routeGroup: '_static',

    // Whether to support decoding of Chinese paths (default false)
    decodePath: true,

    // Prefix path correction. If set to xyz, it automatically corrects to /xyz.
    // prepath : '',

    // Maximum file size to cache (in bytes); files larger than this will not be cached.
    // Valid range: 10000 ~ 500000000. Values outside this range are reset to the
    // default 10000000 (reset, not clamped to the boundary).
    maxFileSize: 12_000_000,

    // Set the value of the cache-control header, defaults to null
    cacheControl: 'max-age=3600',
    
    // Maximum total cache size (default 120MB)
    maxCacheSize: 120_000_000,

    // Failure limit and cache release probability
    failedLimit: 50,
    prob: 6
})

st.init(app)

// Automatically added to the _static group.
// Example: If favicon.ico is in the public directory, it can be accessed via this request.
app.get('/favicon.ico', async c => {}, {group: '_static'})

Interface Description:

  • addType(obj): Add mapping from extension to content-type.
  • clearCache(): Clear the cache.

3. ToFile (File Upload Objectification)

Equips uploaded file objects with a toFile method, facilitating file saving in an object-oriented style.

const Topbit = require('topbit')
const {ToFile} = Topbit.extensions

app.use( new ToFile() )

app.post('/upload', async c => {
    // Get the uploaded file object
    let f = c.getFile('image')

    if (!f) {
        return c.status(400).oo('image file not found')
    }

    // Move the file to the 'images' directory (ensure the directory exists).
    // The second parameter is optional to specify a filename; defaults to a unique name based on timestamp and random number.
    let fname = await f.toFile('./images')

    // Return filename
    c.ok(fname)
})

app.run(1234)

4. SSE (Server-Side Messaging)

Server-Sent Events (SSE) middleware based on the HTTP protocol, used for the server to proactively push data streams to the client.

Basic Usage:

const Topbit = require('topbit')
const {SSE} = Topbit.extensions

let s = new SSE({
  // Timeout 20 seconds; connection closes automatically after this time
  timeout: 20000,

  // Reconnection time; default is 0 meaning do not reconnect
  retry: 5000,

  // Timer interval; execute the handler function every 2 seconds
  timeSlice: 2000
})

// Set handler function, ctx is the request context
s.handle = (ctx) => {
    // Send a single message
    ctx.sendmsg({event: 'eat', data: 'Rice'})
    
    if (Date.now() % 5 === 0) {
        // Send multiple messages. Strings or numbers are automatically converted to message format.
        // Default event is 'message'.
        ctx.sendmsg([
            {event: 'clock', data: Date.now()},
            'May we all be blessed with longevity.',
            Math.random()
        ])
    }
}

// Enable middleware only for route group 'sse'
app.use( s, {group: 'sse'} )

app.get('/sse', async ctx => {}, {group: 'sse'})

app.run(1234)

Generator Mode: Suitable for scenarios requiring dynamic interval adjustment or precise control.

let s = new SSE({
  timeout: 20000,
  retry: 5000,
  timeSlice: 10,
  mode: 'yield' // Enable generator mode
})

s.handle = (ctx) => {
    ctx.sendmsg({event: 'eat', data: 'Rice'})
    // Use await ctx.sse.moment(ms) for delay
    await ctx.sse.moment(100)
}

5. Proxy (HTTP/1.1 Reverse Proxy)

Reverse proxy for HTTP/1.1 protocol, supporting load balancing and Alive Check.

port option (optional): Tell the proxy the listening port so it auto-appends the port to hostname keys in config; requests are then matched directly against the Host header without port parsing.

  • Default ''; 0 and empty string mean no auto-appending (the config key must exactly match the Host header).
  • Can be a number or numeric string, range 1 ~ 65535.
  • Non-80/443 ports: bare hostname 'a.com' is replaced with 'a.com:port'; a config key already ending with :port is kept as-is.
  • 80/443 ports: regardless of whether the config key is bare 'a.com' or suffixed 'a.com:443', both keys ('a.com' + 'a.com:443') are generated and share the same backend config, so Host headers with or without the port both match.
  • When port is empty but the config key ends with :80/:443, the corresponding bare key is also auto-generated and both share.
  • When both a bare key and a suffixed key are configured, the first one wins; the latter's backend config is appended to the same array.

Basic Proxy Configuration:

const Topbit = require('topbit')
const {Proxy} = Topbit.extensions

let hostcfg = {
    // Simple mapping: default path is /
    'a.com' : 'http://localhost:8001',

    // Specific path mapping
    'b.com' : {
        path : '/xyz',
        url : 'http://localhost:8002'
    },

    // Complex config and rewrite
    'c.com' : [
        {
            path : '/',
            url : 'http://localhost:8004',
            rewrite: (ctx, path) => {
              // Custom route rewrite rules
              if (path.indexOf('/xyz') === 0) {
                return path.replace('/xyz', '/w')
              }
            }
        }
    ]
};

const pxy = new Proxy({
    timeout: 10000,
    // Tell the listening port so hostname keys get the port appended
    port: 1234,
    config: hostcfg
})

pxy.init(app)

app.run(1234)

Path matching and cross-host fallback:

The path in a backend entry is normalized into a prefix route: /api/api/*, //* (parameter routes containing /: are kept as-is). Paths from all hosts are merged into a single global route table and registered with the framework; each distinct path is registered exactly once, so multiple hosts declaring the same path never conflict.

Because route matching happens before middleware and is not Host-aware, the c.routepath handed to the proxy is the globally most specific one, which is not necessarily a path the current host declared. The proxy automatically falls back to the longest prefix that host did declare:

const pxy = new Proxy({
    port: 443,
    config: {
        'a.com': [
            { path: '/api',    url: 'http://127.0.0.1:8001' },
            { path: '/static', url: 'http://127.0.0.1:8002' }
        ],
        'b.com': [{ path: '/',       url: 'http://127.0.0.1:8003' }],
        'c.com': [{ path: '/api/v2', url: 'http://127.0.0.1:8004' }]
    }
})

| Request | Matched routepath | Fallback | Forwarded to | |---|---|---|---| | a.com/api/x | /api/* | direct hit | a's /api backend | | a.com/api/v2/x | /api/v2/* | → /api/* | a's /api backend | | a.com/other | /* | nothing to fall back to | no match | | b.com/api/v2/x | /api/v2/* | → /* | b's / backend, full path /api/v2/x forwarded | | c.com/api/x | /api/* | nothing to fall back to | no match |

Fallback rules:

  • The fallback map is computed once in init(); at runtime it costs a single hash lookup.
  • Only paths the proxy itself registered are eligible. Your own application routes (e.g. app.get('/admin/:id')) are not in the map and will never be swallowed by the proxy.
  • It only falls back to a path that host actually declared — no backend is invented.
  • If there is nothing to fall back to, the request is treated as unmatched.
  • After a fallback, c.routepath is rewritten to the path actually used, so later middleware sees the real value.
  • Parameter routes (those not ending in /*) do not participate.

defaultServer (default host):

Specifies the fallback target when the request Host matches no configured host. The typical use is routing traffic for retired domains to one canonical domain.

const pxy = new Proxy({
    port: 443,
    defaultServer: 'main.com',       // 'https://main.com' also works; the scheme is stripped
    config: {
        'a.com'   : 'http://127.0.0.1:8001',
        'main.com': 'http://127.0.0.1:8009'
    }
})
  • The value is a host name, not a URL. Forms like 'https://main.com/x' have the scheme and path stripped, leaving main.com.
  • Works together with port: you may give the bare host exactly as written in config, and the proxy appends the port before matching.
  • If the normalized value is not present in config, a warning is printed and the option is ignored, leaving behavior unchanged.
  • It only applies when the Host does not match. A matching Host with no matching path does not fall back — that means "this domain has no such path".
  • The host fallback happens before the path fallback, so the fallback target host also benefits from path fallback.
  • HTTP/1.0 requests with no Host header are caught by defaultServer as well.

Handling unmatched requests:

When the proxy does not match (neither Host nor path), it calls next() and hands the request back to the middleware chain. The routes registered by init() belong to the group topbit_proxy, so adding a middleware to that group lets you customise the unmatched response:

// Works the same for Proxy / ProxyNoAgent / Http2Proxy
app.use(async (c, next) => {
    return c.status(404).html('<h1>404 Not Found</h1>')
}, '@topbit_proxy')

Three properties of this middleware:

  • It sits downstream of the proxy middleware, so it does not run when the proxy matches.
  • It only applies to routes the proxy registered and never affects your own application routes.
  • It is orthogonal to defaultServer: an unmatched Host is taken over by defaultServer first; only a matched Host with an unmatched path reaches this middleware.

If full mode is on and defaultServer is not configured, an unmatched request returns 502 directly instead of calling next().

Load Balancing Configuration: Implemented via configuration arrays, supporting weight and alive checks.

let load_balance_cfg = {
    'a.com' : [
        {
            path : '/',
            url : 'http://localhost:1234',
            // Check service liveness every 3 seconds
            aliveCheckInterval : 3,
            aliveCheckPath : '/alive-check',
            // Weight: larger numbers mean higher weight
            weight: 3
        },
        {
            path : '/',
            url : 'http://localhost:1235',
            aliveCheckInterval : 3,
            aliveCheckPath : '/ok',
            weight: 2
        }
    ]
}

const pxy = new Proxy({
    timeout: 10000,
    config: load_balance_cfg
})

pxy.init(app)

Request body size limit: the proxy is a pre middleware and streams the request body to the backend before the framework ever reads it, so the framework's maxBody does not apply to proxied requests — the proxy enforces its own limit.

Precedence (same shape as requestTimeout):

| Priority | Source | Description | |---|---|---| | 1 | ctx.box.proxyMaxBody | Set by a pre middleware, applies to that one request | | 2 | backend option maxBody | Per-backend override | | 3 | proxy option maxBody | Defaults to 50000000 (50MB) |

The comparison matches the framework's: accumulated bytes against the limit, so 0 means no request body is allowed. A request that declares a content-length over the limit is rejected up front (no backend connection is opened); chunked requests are counted while forwarding and aborted as soon as they exceed it. Both return 413.

// The pre middleware must be registered BEFORE pxy.init(app) to run ahead of the proxy
app.use(async (c, next) => {
    if (c.path.startsWith('/upload')) c.box.proxyMaxBody = 100 * 1024 * 1024
    return await next(c)
}, {pre: true})

const pxy = new Proxy({
    maxBody: 2 * 1024 * 1024,   // instance default
    config: {
        'a.com': [
            { url: 'http://127.0.0.1:1234', maxBody: 20 * 1024 * 1024 }  // per-backend override
        ]
    }
})

pxy.init(app)

Full list of constructor options:

| Option | Default | Description | |---|---|---| | config / host | {} | Proxy table; key is a host, value is a string, object, or array of objects | | port | '' | Listening port, used to auto-append to the keys of config; see above | | defaultServer | '' | Fallback host when the Host does not match; see above | | timeout | 35000 | Timeout for a single backend request (ms); can be overridden per backend | | requestTimeout | 600000 | Overall transfer time limit (ms), counted from when response headers arrive; 0 means unlimited | | maxBody | 50000000 | Request body limit in bytes; see above | | methods | ['GET','POST','PUT','DELETE','OPTIONS','HEAD','PATCH','TRACE'] | Methods used when registering routes | | full | false | Full-proxy mode: an unmatched request returns 502 instead of calling next(). When defaultServer is configured the fallback takes precedence | | addIP | false | When true, append the client IP to an existing real-IP header (comma-separated chain) instead of overwriting it | | realIPHeader | 'x-real-ip' | Name of the header carrying the client IP | | balancer | null | Custom load balancer providing select(c, prlist, pxybalance). Topbit.ProxyBalancer (identity-based consistent hashing) is built in | | connectOptions | {family: 4} | Connection options passed to the underlying request; each key can be overridden per backend | | maxSockets | 1000 | Max sockets per backend Agent | | maxFreeSockets | 100 | Max free sockets per backend Agent | | agentKeepAlive | true | Whether the backend Agent uses keepAlive | | autoClearListeners | false | Remove listeners from the backend response object after forwarding completes | | debug | false | Print debug output |

Backend options (each entry in config):

| Option | Default | Description | |---|---|---| | url | required | Backend address; http://, https:// and unix:// are supported | | path | '/' | Match path, normalized into a prefix route | | weight | 1 | Weight; higher values are selected more often | | timeout | inherits instance | Per-request timeout | | requestTimeout | inherits instance | Overall transfer time limit | | maxBody | inherits instance | Request body limit | | headers | {} | Extra request headers when forwarding; overrides the client's on conflict | | resHeaders | null | Extra headers added to the response | | resHeadersCallback | null | Function to post-process backend response headers | | rewrite | null | Function (ctx, path) returning the rewritten path; a falsy return means no rewrite | | aliveCheckPath | '/' | Path used by the alive check request | | aliveCheckInterval | 5 | Alive check interval in seconds, range 0 ~ 7200; 0 disables it | | aliveCheckMethod | 'GET' | Method used by the alive check; one of GET, HEAD, TRACE | | connectOptions | inherits instance | Connection options for this backend only |

About ProxyNoAgent: the extensions also ship ProxyNoAgent, used exactly like Proxy except that it keeps no per-backend http(s).Agent and relies on the global agent instead. Therefore maxSockets / maxFreeSockets / agentKeepAlive from the table above do not apply to it; the instance-level keepAlive (default true) takes their place.

Since Node.js >= 19 http.globalAgent defaults to keepAlive: true, so ProxyNoAgent still reuses connections by default. Pass keepAlive: false explicitly if you really want one connection per request.

const {ProxyNoAgent} = Topbit.extensions

const pxy = new ProxyNoAgent({
    config: { 'a.com': 'http://127.0.0.1:1234' },
    keepAlive: false  // disable connection reuse
})

6. Http2Proxy (HTTP/2 Reverse