simple HTTP | HTTPS | WS | WSS reverse proxy in node.js. currently supports:
- reverse proxy using incoming request's
x-forwarded-hostorhostheader to pre-configured origin servers. see Host and X-Forwarded-Host - proxy incoming
http:orhttps:request to originhttp:orhttps:servers - proxy incoming
ws:orwss:request to originws:orwss:servers - supports graceful shutdowns
- supports round-robin origin server selection (optional TCP health checks)
- supports force upgrade
http:tohttps:andws:towss: - supports forced redirects for host migration
- supports URL parts preservation during redirects
- multi-core via
node:cluster(workersconfig; default = CPU count) - keep-alive outbound agents, stream backpressure, and configurable timeouts
- async structured JSON logging (
logLevel, optionalaccessLog) - optional metrics (
metricsEnabled/ localhostadminPort→GET /metrics)
Requires Node.js >= 20.
- for global installation
npm i fimiproxy -g - for local installation
npm i fimiproxy - for local dev-dependency installation
npm i fimiproxy -D
replace npm with yarn or any other package manager of choice.
{
"exposeHttpProxy": false,
"httpPort": "",
"exposeHttpsProxy": false,
"httpsPort": "",
"exposeWsProxyForHttp": false,
"exposeWsProxyForHttps": false,
"httpsPublicKeyFilepath": "",
"httpsPrivateKeyFilepath": "",
"httpsPublicKey": "",
"httpsPrivateKey": "",
"debug": false,
"workers": 1,
"originTimeoutMs": 30000,
"headersTimeoutMs": 60000,
"requestTimeoutMs": 0,
"keepAliveTimeoutMs": 5000,
"maxSockets": 256,
"maxFreeSockets": 256,
"maxConnections": 0,
"accessLog": false,
"logLevel": "info",
"originHealthEnabled": false,
"originHealthIntervalMs": 10000,
"metricsEnabled": false,
"metricsLogIntervalMs": 60000,
"adminPort": "",
"routes": [
{
"origin": [
{
"originHost": "",
"originPort": "",
"originProtocol": "http:"
},
{
"originHost": "",
"originPort": "",
"originProtocol": "ws:"
}
],
"incomingHostAndPort": "",
"forceUpgradeHttpToHttps": false,
"forceUpgradeWsToWss": false,
"forceRedirect": false,
"usePermanentRedirect": false,
"redirectHost": "",
"redirectURLParts": false,
"overrideHost": ""
}
],
"forceUpgradeHttpToHttps": false,
"forceUpgradeWsToWss": false,
"usePermanentRedirect": false,
"redirectHost": "",
"redirectURLParts": false
}exposeHttpProxy— set totrueto expose an HTTP server, requireshttpPortto be set iftrueexposeHttpsProxy— set totrueto expose an HTTPS server, requireshttpsPort,httpsPublicKeyORhttpsPublicKeyFilepath,httpsPrivateKeyORhttpsPrivateKeyFilepathto be set iftrueexposeWsProxyForHttp— set totrueto expose a WebSocket server for HTTP requests, requireshttpPortandexposeHttpProxyto be set iftrueexposeWsProxyForHttps— set totrueto expose a WebSocket server for HTTPS requests, requireshttpsPortandexposeHttpsProxyto be set iftruehttpPort— port HTTP server should listen on, whenexposeHttpProxyistruehttpsPort— port HTTPS server should listen on, whenexposeHttpsProxyistruehttpsPublicKeyFilepath— filepath to TLS certificate (public key) used with HTTPS serverhttpsPrivateKeyFilepath— filepath to TLS private key used with HTTPS serverhttpsPublicKey— TLS certificate (public key) string used with HTTPS server. takes precedence overhttpsPublicKeyFilepathhttpsPrivateKey— TLS private key string used with HTTPS server. takes precedence overhttpsPrivateKeyFilepathdebug— set totrueto enable debug logging for troubleshooting (also forceslogLeveltodebug)workers— number of cluster worker processes. Default:os.availableParallelism(). Set to1for a single process (recommended for tests and simple local use). Whenworkers > 1, a primary process forks workers that share the listen ports; keep your process manager / deploycountat 1 (do not run multiple independent masters on :80/:443)originTimeoutMs— timeout for origin HTTP requests and WebSocket opens (default30000)headersTimeoutMs— inbound headers timeout (default60000)requestTimeoutMs— inbound request timeout;0disables (default0)keepAliveTimeoutMs— inbound keep-alive timeout (default5000)maxSockets/maxFreeSockets— outbound keep-alivehttp/httpsagent pool sizes (default256)maxConnections— max concurrent inbound connections;0= unlimited (default0). Excess connections are dropped by NodeaccessLog— whentrue, emit structured per-request access lines (defaultfalse)logLevel—error|warn|info|debug(defaultinfo)originHealthEnabled— whentrue, periodically TCP-check origins and skip unhealthy ones in round-robin (defaultfalse)originHealthIntervalMs— health check interval (default10000)metricsEnabled— whentrue, periodically log a metrics snapshot (defaultfalse)metricsLogIntervalMs— metrics log interval (default60000)adminPort— if set, listen on127.0.0.1:adminPortand serveGET /metricsas JSONforceUpgradeHttpToHttps— set totrueto force upgrade allhttp:requests tohttps:requests globallyforceUpgradeWsToWss— set totrueto force upgrade allws:requests towss:requests globallyusePermanentRedirect— set totrueto use permanent redirect globally. The proxy server will return a308redirect response to the client instead of the default307temporary redirect responseredirectHost— default host to redirect to globally, e.g. when upgrading to HTTPS or WSS, or if the incoming host is no longer supported and all requests to it should be redirected somewhere else. if not set, the proxy server will redirect to the incomingx-forwarded-hostorhostheader fieldredirectURLParts— controls which URL parts are preserved during redirects. Can betrue(preserve all parts),false(preserve only host), or an object specifying which parts to preserve (see Route-level Configuration below)
Logs are structured JSON lines written asynchronously to stdout/stderr. Typical fields: level, msg, time, pid, optional workerId, plus event-specific fields (host, origin, status, durationMs, errors).
- Default: startup/route configuration + errors
accessLog: true: one access line per completed HTTP/WS requestdebug: true/logLevel: "debug": routing and origin option details
routes— array of incoming host to origin protocol, host, and port mappingsorigin— array of origin server host, port, and protocol (supports round-robin load balancing)originHost— origin host or IP addressoriginPort— origin port numberoriginProtocol— origin protocol. one ofhttp:,https:,ws:, orwss:. don't forget the:at the end
incomingHostAndPort— incominghost:portpattern to match for proxying to origin server. picked from HTTPhostheader field. Examples:example.com:80,api.example.com,*.example.com(wildcards supported)forceUpgradeHttpToHttps— set totrueto force upgradehttp:requests tohttps:requests for this routeforceUpgradeWsToWss— set totrueto force upgradews:requests towss:requests for this routeforceRedirect— set totrueto force redirect all requests to this route to theredirectHost. useful for permanent host migrationsusePermanentRedirect— set totrueto use permanent redirect for this route. The proxy server will return a308redirect response to the client instead of the default307temporary redirect responseredirectHost— host to redirect to for this route, e.g. when upgrading to HTTPS or WSS, or whenforceRedirectis enabled. if not set, the proxy server will redirect to the incomingx-forwarded-hostorhostheader fieldredirectURLParts— controls which URL parts are preserved during redirects for this route. Can be:true— preserve all URL parts (protocol, pathname, search, username, password)false— preserve only the host- An object with specific parts:
{ "protocol": true, "pathname": true, "search": false, "username": false, "password": false }
overrideHost— if set, the proxy will override thehostandx-forwarded-hostheader fields in requests sent to the origin server. useful for testing or when a specific host is required (e.g., for OAuth callbacks)
{
"exposeHttpProxy": true,
"httpPort": "80",
"exposeHttpsProxy": true,
"httpsPort": "443",
"httpsPublicKeyFilepath": "/path/to/cert.pem",
"httpsPrivateKeyFilepath": "/path/to/key.pem",
"routes": [
{
"origin": [
{
"originHost": "localhost",
"originPort": 3000,
"originProtocol": "http:"
}
],
"incomingHostAndPort": "example.com",
"forceUpgradeHttpToHttps": true
}
]
}{
"exposeHttpsProxy": true,
"httpsPort": "443",
"httpsPublicKey": "-----BEGIN CERTIFICATE-----\n...",
"httpsPrivateKey": "-----BEGIN PRIVATE KEY-----\n...",
"routes": [
{
"origin": [
{
"originHost": "backend1.internal",
"originPort": 8080,
"originProtocol": "http:"
},
{
"originHost": "backend2.internal",
"originPort": 8080,
"originProtocol": "http:"
}
],
"incomingHostAndPort": "api.example.com"
}
]
}{
"exposeHttpProxy": true,
"httpPort": "80",
"routes": [
{
"origin": [],
"incomingHostAndPort": "old-domain.com",
"forceRedirect": true,
"redirectHost": "new-domain.com",
"usePermanentRedirect": true,
"redirectURLParts": {
"pathname": true,
"search": true
}
}
]
}{
"exposeHttpsProxy": true,
"httpsPort": "443",
"exposeWsProxyForHttps": true,
"httpsPublicKeyFilepath": "/path/to/cert.pem",
"httpsPrivateKeyFilepath": "/path/to/key.pem",
"routes": [
{
"origin": [
{
"originHost": "websocket-server.internal",
"originPort": 8080,
"originProtocol": "ws:"
}
],
"incomingHostAndPort": "ws.example.com",
"forceUpgradeWsToWss": true
}
]
}- if installed globally, run
fimiproxy ./path/to/config.json - if installed locally, run
npm exec fimiproxy ./path/to/config.json - for one-time run, run
npx -y fimiproxy ./path/to/config.json
Alternatively, you can start fimiproxy without passing a config filepath argument by setting the FIMIPROXY_CONFIG_FILEPATH environment variable:
# Set the environment variable
export FIMIPROXY_CONFIG_FILEPATH=./path/to/config.json
# Then run fimiproxy without arguments
fimiproxyOr in a single command:
FIMIPROXY_CONFIG_FILEPATH=./path/to/config.json fimiproxyNote: The command line argument takes precedence over the environment variable. If both are provided, the command line argument will be used.
import fimiproxy from "fimiproxy"
// start fimiproxy
await fimiproxy.startFimiproxyUsingConfig({
/** config */ {
exposeHttpProxy: true,
exposeHttpsProxy: true,
httpPort: "80",
httpsPort: "443",
workers: 1,
debug: false,
routes: [{
origin: [{
originHost: "localhost",
originPort: 3000,
originProtocol: "https:",
}],
incomingHostAndPort: "www.example.com",
forceUpgradeHttpToHttps: true,
overrideHost: "localhost:3000"
}],
httpsPublicKey: "-----BEGIN CERTIFICATE-----\n...",
httpsPrivateKey: "-----BEGIN PRIVATE KEY-----\n...",
},
/** shouldHandleGracefulShutdown */ true,
/** exitProcessOnShutdown */ true,
});
// end fimiproxy
await fimiproxy.endFimiproxy(/** exitProcessOnShutdown */ true);startFimiproxyUsingConfig— start fimiproxy using configconfig: FimiproxyRuntimeConfig— see configuration aboveshouldHandleGracefulShutdown— defaults totrue. iftrue, will listen forSIGINTandSIGTERM, and attempt to gracefully shut down the proxy server. Whenfalse, forces single-process mode (useful for tests)exitProcessOnShutdown— defaults totrue. ifshouldHandleGracefulShutdownistrue, will callprocess.exit()after graceful shutdown. your process may not shut down afterSIGINTandSIGTERMif nottrue. currently untested behaviour (if process will shutdown or not) when set tofalseandshouldHandleGracefulShutdownistrue
startFimiproxyUsingConfigFile— start fimiproxy using config read from filepathfilepath: string— file at filepath should be a json file, see configuration section above
startFimiproxyUsingProcessArgs— start fimiproxy using filepath picked fromprocess.argv[2]see https://nodejs.org/docs/latest/api/process.html#processargv. example,node your-script.js ./path/to/config.jsonstartFimiproxyUsingEnvVar— start fimiproxy using filepath from environment variable (defaults toFIMIPROXY_CONFIG_FILEPATH)endFimiproxy— gracefully end fimiproxy (returns a Promise)exitProcess— defaults totrue. callsprocess.exit()iftrue
setupGracefulShutdown— register SIGINT/SIGTERM handlers without shutting down immediately (used internally whenshouldHandleGracefulShutdownis true)
Use fimiproxy to proxy local development servers with SSL termination:
fimiproxy dev-config.jsonRoute different subdomains to different microservices:
api.example.com→ backend API servicews.example.com→ WebSocket servicecdn.example.com→ static file server
Gradually migrate from old domain to new domain while preserving SEO:
- Use
forceRedirectwithusePermanentRedirect: true - Preserve URL paths and query parameters with
redirectURLParts
Distribute traffic across multiple backend servers using round-robin selection. Enable originHealthEnabled to skip origins that fail periodic TCP checks (if all are unhealthy, fimiproxy falls back to the full origin list so traffic is not hard-stopped).
- Prefer
workersequal to CPU count on production hosts that terminate TLS (HTTPS benefits most from multi-core). - Keep deploy/process-manager instance count at 1; multi-core is internal cluster, not multiple masters binding the same privileged ports.
- Leave
accessLogoff unless you need it; errors still log aterrorlevel. - Tune
maxSocketsfor busy localhost fan-out; setmaxConnectionsif you need overload protection. - Run a quick load check with
npm run bench(see benchmarks).
npm run bench
npm run bench -- --duration 20 --connections 100The in-process harness uses workers: 1. To compare multi-worker RPS, start fimiproxy via the CLI with "workers": N in config and point autocannon (or similar) at that process.
Set debug: true in your configuration or use the FIMIPROXY_DEBUG=true environment variable to see detailed logs. Alternatively set "logLevel": "debug".
-
EADDRINUSE Error: Port already in use
- Check if another process is using the port:
lsof -i :PORT - Use different ports in your configuration
- Do not raise external process
countabove 1 for the same :80/:443 — useworkersinstead
- Check if another process is using the port:
-
SSL Certificate Issues:
- Ensure certificate files exist and are readable
- Verify certificate format (PEM)
- Check certificate expiration
-
WebSocket Connection Issues:
- Ensure
exposeWsProxyForHttporexposeWsProxyForHttpsis enabled - Verify origin server supports WebSocket protocol
- Check for protocol mismatch (ws vs wss)
- Origin open failures time out after
originTimeoutMs
- Ensure
-
Host Header Issues:
- Use
overrideHostif the origin server expects specific host headers - Check that
incomingHostAndPortmatches the actual request host
- Use
- Call
endFimiproxy()before anotherstartFimiproxy*in the same process. State is held on a per-processFimiproxyInstance; overlapping starts without teardown are unsupported. - Round-robin is simple rotation (not weighted or sticky). Optional TCP health checks can skip unhealthy origins when
originHealthEnabledis true. - Wildcard hosts (
*.example.com) are documented historically; matching is exactincomingHostAndPortlookup today.