@lopatnov/express-reverse-proxy 5.0.6 → 5.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +102 -85
  2. package/package.json +1 -1
  3. package/server.js +24 -3
package/README.md CHANGED
@@ -269,14 +269,14 @@ If `server-config.json` already exists, the command asks before overwriting.
269
269
 
270
270
  Manage the PM2 process cluster. Action defaults to `start` when omitted.
271
271
 
272
- | Action | Description |
273
- | --------- | ------------------------------------------------------ |
274
- | `start` | Start the cluster (default when action is omitted) |
275
- | `stop` | Stop all cluster instances |
276
- | `restart` | Restart all cluster instances |
277
- | `status` | Show PM2 process status table |
278
- | `logs` | Stream the last 200 log lines |
279
- | `monitor` | Open the PM2 real-time monitor |
272
+ | Action | Description |
273
+ | --------- | -------------------------------------------------- |
274
+ | `start` | Start the cluster (default when action is omitted) |
275
+ | `stop` | Stop all cluster instances |
276
+ | `restart` | Restart all cluster instances |
277
+ | `status` | Show PM2 process status table |
278
+ | `logs` | Stream the last 200 log lines |
279
+ | `monitor` | Open the PM2 real-time monitor |
280
280
 
281
281
  ```shell
282
282
  express-reverse-proxy --cluster # same as --cluster start
@@ -325,10 +325,10 @@ Add a `$schema` reference to your config file to get property autocomplete, desc
325
325
 
326
326
  ### Environment variables
327
327
 
328
- | Variable | Default | Description |
329
- |----------|---------|-------------|
330
- | `PORT` | `8000` | Overrides the port when it is not set in the config file |
331
- | `NODE_ENV` | — | Passed through to PM2 env profiles (`env` / `env_development`) |
328
+ | Variable | Default | Description |
329
+ | ---------- | ------- | -------------------------------------------------------------- |
330
+ | `PORT` | `8000` | Overrides the port when it is not set in the config file |
331
+ | `NODE_ENV` | — | Passed through to PM2 env profiles (`env` / `env_development`) |
332
332
 
333
333
  ### port
334
334
 
@@ -360,10 +360,10 @@ Controls HTTP request logging (Morgan). Enabled by default (`dev` format). Set t
360
360
  }
361
361
  ```
362
362
 
363
- | Option | Default | Description |
364
- | -------- | ------------ | -------------------------------------------------------------- |
365
- | `format` | `"combined"` | Morgan format: `combined`, `common`, `dev`, `short`, or `tiny` |
366
- | `file` | none | Path to log file (relative to config file). Appended if exists |
363
+ | Option | Default | Description |
364
+ | -------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
365
+ | `format` | `"dev"` (console) / `"combined"` (file) | Morgan format: `combined`, `common`, `dev`, `short`, or `tiny`. Defaults to `"combined"` when `file` is set, `"dev"` otherwise |
366
+ | `file` | none | Path to log file (relative to config file). Appended if exists |
367
367
 
368
368
  When `file` is set, logs are written to the file only (not to the console).
369
369
 
@@ -383,10 +383,10 @@ Watches the `folders` directories for file changes and automatically reloads con
383
383
 
384
384
  The server exposes two endpoints when hot reload is enabled:
385
385
 
386
- | Endpoint | Description |
387
- | --------------------------------- | ------------------------------------------ |
388
- | `GET /__hot-reload__` | SSE stream — browsers subscribe here |
389
- | `GET /__hot-reload__/client.js` | Ready-to-use client script |
386
+ | Endpoint | Description |
387
+ | ------------------------------- | ------------------------------------ |
388
+ | `GET /__hot-reload__` | SSE stream — browsers subscribe here |
389
+ | `GET /__hot-reload__/client.js` | Ready-to-use client script |
390
390
 
391
391
  #### Connecting the client
392
392
 
@@ -399,7 +399,7 @@ The server exposes two endpoints when hot reload is enabled:
399
399
  **Option B — bundled project** (Vite, webpack, etc.): import the client module. The bundler resolves it through the package `exports` field:
400
400
 
401
401
  ```js
402
- import '@lopatnov/express-reverse-proxy/hot-reload-client';
402
+ import "@lopatnov/express-reverse-proxy/hot-reload-client";
403
403
  ```
404
404
 
405
405
  Both options connect to `/__hot-reload__` and call `location.reload()` when a file change is detected. The connection is re-established automatically after 3 seconds if the server restarts.
@@ -446,11 +446,11 @@ Permanently or temporarily redirect URL paths to new destinations. Redirects are
446
446
  }
447
447
  ```
448
448
 
449
- | Field | Default | Description |
450
- | -------- | ------- | -------------------------------------------------------- |
451
- | `from` | — | Source URL path *(array form only, required)* |
452
- | `to` | — | Destination path or full URL *(required)* |
453
- | `status` | `301` | HTTP redirect status: `301`, `302`, `307`, or `308` |
449
+ | Field | Default | Description |
450
+ | -------- | ------- | --------------------------------------------------- |
451
+ | `from` | — | Source URL path _(array form only, required)_ |
452
+ | `to` | — | Destination path or full URL _(required)_ |
453
+ | `status` | `301` | HTTP redirect status: `301`, `302`, `307`, or `308` |
454
454
 
455
455
  > `301` — Moved Permanently. `302` — Found (temporary). Use `301` for permanent URL changes and `302` for temporary ones.
456
456
 
@@ -538,7 +538,11 @@ Forward requests to a back-end server. Supports three forms:
538
538
  ```json
539
539
  {
540
540
  "proxy": {
541
- "/api": ["http://backend1:3000", "http://backend2:3000", "http://backend3:3000"]
541
+ "/api": [
542
+ "http://backend1:3000",
543
+ "http://backend2:3000",
544
+ "http://backend3:3000"
545
+ ]
542
546
  }
543
547
  }
544
548
  ```
@@ -629,8 +633,8 @@ Enable HTTPS on a port by adding an `ssl` object to any site config for that por
629
633
  | ---------- | --------- | -------------------------------------------------------- |
630
634
  | `key` | `string` | Path to the private key file (PEM format) |
631
635
  | `cert` | `string` | Path to the certificate file (PEM format) |
632
- | `ca` | `string` | *(optional)* Path to the CA bundle for client validation |
633
- | `redirect` | `integer` | *(optional)* HTTP port to redirect (301) to HTTPS |
636
+ | `ca` | `string` | _(optional)_ Path to the CA bundle for client validation |
637
+ | `redirect` | `integer` | _(optional)_ HTTP port to redirect (301) to HTTPS |
634
638
 
635
639
  Paths are resolved **relative to the config file**, not the current working directory.
636
640
 
@@ -780,11 +784,11 @@ Limit the number of requests a client can make in a time window. Responds with `
780
784
  }
781
785
  ```
782
786
 
783
- | Option | Default | Description |
784
- | ----------- | -------- | ------------------------------------------------ |
785
- | `windowMs` | `60000` | Time window in milliseconds |
786
- | `limit` | `5` | Maximum requests per client per window |
787
- | `message` | built-in | Response body when limit is exceeded |
787
+ | Option | Default | Description |
788
+ | ---------- | -------- | -------------------------------------- |
789
+ | `windowMs` | `60000` | Time window in milliseconds |
790
+ | `limit` | `5` | Maximum requests per client per window |
791
+ | `message` | built-in | Response body when limit is exceeded |
788
792
 
789
793
  See [express-rate-limit docs](https://express-rate-limit.mintlify.app/reference/configuration) for all options.
790
794
 
@@ -805,11 +809,11 @@ Protect the site with HTTP Basic Authentication. All requests must include valid
805
809
  }
806
810
  ```
807
811
 
808
- | Option | Default | Description |
809
- | ----------- | ------- | ------------------------------------------------------------ |
810
- | `users` | — | Object mapping username → password *(required)* |
812
+ | Option | Default | Description |
813
+ | ----------- | ------- | -------------------------------------------------------------- |
814
+ | `users` | — | Object mapping username → password _(required)_ |
811
815
  | `challenge` | `false` | Send `WWW-Authenticate` header to trigger browser login dialog |
812
- | `realm` | — | Realm string shown in the browser login dialog |
816
+ | `realm` | — | Realm string shown in the browser login dialog |
813
817
 
814
818
  See [express-basic-auth docs](https://github.com/LionC/express-basic-auth#options) for all options.
815
819
 
@@ -841,8 +845,8 @@ Custom path:
841
845
  }
842
846
  ```
843
847
 
844
- | Option | Default | Description |
845
- | ------ | -------------- | -------------------------------- |
848
+ | Option | Default | Description |
849
+ | ------ | --------------- | ------------------------------- |
846
850
  | `path` | `"/__health__"` | URL path of the health endpoint |
847
851
 
848
852
  > The health check endpoint is placed before rate limiting and basic auth — it is always publicly accessible regardless of other authentication settings.
@@ -867,12 +871,12 @@ Execute server-side scripts using the CGI (Common Gateway Interface) protocol. W
867
871
  }
868
872
  ```
869
873
 
870
- | Option | Default | Description |
871
- | -------------- | ------------------------------------- | -------------------------------------------------------------------- |
872
- | `path` | `"/cgi-bin"` | URL prefix that triggers CGI dispatch |
873
- | `dir` | `"./cgi-bin"` | Local directory containing scripts (resolved relative to config file)|
874
- | `extensions` | `[".cgi", ".pl", ".py", ".sh"]` | File extensions treated as executable CGI scripts |
875
- | `interpreters` | `{}` | Map of file extension → interpreter command |
874
+ | Option | Default | Description |
875
+ | -------------- | ------------------------------- | --------------------------------------------------------------------- |
876
+ | `path` | `"/cgi-bin"` | URL prefix that triggers CGI dispatch |
877
+ | `dir` | `"./cgi-bin"` | Local directory containing scripts (resolved relative to config file) |
878
+ | `extensions` | `[".cgi", ".pl", ".py", ".sh"]` | File extensions treated as executable CGI scripts |
879
+ | `interpreters` | `{}` | Map of file extension → interpreter command |
876
880
 
877
881
  Shorthand — point directly to the script directory (all defaults apply):
878
882
 
@@ -884,18 +888,18 @@ Shorthand — point directly to the script directory (all defaults apply):
884
888
 
885
889
  CGI environment variables set for every request:
886
890
 
887
- | Variable | Value |
888
- | ----------------- | -------------------------------------------------------- |
889
- | `REQUEST_METHOD` | HTTP method (`GET`, `POST`, …) |
890
- | `QUERY_STRING` | URL query string (without `?`) |
891
- | `CONTENT_TYPE` | `Content-Type` request header |
892
- | `CONTENT_LENGTH` | `Content-Length` request header |
893
- | `SCRIPT_FILENAME` | Absolute path to the script file |
894
- | `SCRIPT_NAME` | URL path to the script (e.g. `/cgi-bin/hello.py`) |
895
- | `SERVER_NAME` | Requested hostname |
896
- | `SERVER_PORT` | Server listen port |
897
- | `REMOTE_ADDR` | Client IP address |
898
- | `HTTP_*` | All request headers (e.g. `HTTP_ACCEPT`, `HTTP_HOST`) |
891
+ | Variable | Value |
892
+ | ----------------- | ----------------------------------------------------- |
893
+ | `REQUEST_METHOD` | HTTP method (`GET`, `POST`, …) |
894
+ | `QUERY_STRING` | URL query string (without `?`) |
895
+ | `CONTENT_TYPE` | `Content-Type` request header |
896
+ | `CONTENT_LENGTH` | `Content-Length` request header |
897
+ | `SCRIPT_FILENAME` | Absolute path to the script file |
898
+ | `SCRIPT_NAME` | URL path to the script (e.g. `/cgi-bin/hello.py`) |
899
+ | `SERVER_NAME` | Requested hostname |
900
+ | `SERVER_PORT` | Server listen port |
901
+ | `REMOTE_ADDR` | Client IP address |
902
+ | `HTTP_*` | All request headers (e.g. `HTTP_ACCEPT`, `HTTP_HOST`) |
899
903
 
900
904
  A minimal Python example (`cgi-bin/hello.py`):
901
905
 
@@ -960,39 +964,52 @@ Shorthand — directory only (all defaults apply):
960
964
  }
961
965
  ```
962
966
 
963
- | Option | Default | Description |
964
- | --------------- | -------------- | ------------------------------------------------------------------------ |
965
- | `path` | `"/upload"` | URL prefix for the upload endpoint |
966
- | `dir` | `"./uploads"` | Save directory (resolved relative to the config file) |
967
- | `maxFileSize` | none | Maximum file size in bytes; responds with `413` when exceeded |
968
- | `maxFiles` | none | Maximum number of files per request; responds with `400` when exceeded |
969
- | `allowedTypes` | none | MIME type whitelist; responds with `400` when the type is not in the list|
970
- | `fieldName` | any field | Accept only files uploaded in this specific form field |
967
+ | Option | Default | Description |
968
+ | -------------- | ------------- | ------------------------------------------------------------------------- |
969
+ | `path` | `"/upload"` | URL prefix for the upload endpoint |
970
+ | `dir` | `"./uploads"` | Save directory (resolved relative to the config file) |
971
+ | `maxFileSize` | none | Maximum file size in bytes; responds with `413` when exceeded |
972
+ | `maxFiles` | none | Maximum number of files per request; responds with `400` when exceeded |
973
+ | `allowedTypes` | none | MIME type whitelist; responds with `400` when the type is not in the list |
974
+ | `fieldName` | any field | Accept only files uploaded in this specific form field |
971
975
 
972
976
  **Array form** — multiple upload endpoints on the same site:
973
977
 
974
978
  ```json
975
979
  {
976
980
  "upload": [
977
- { "path": "/photos", "dir": "./photos", "allowedTypes": ["image/jpeg", "image/png"] },
978
- { "path": "/docs", "dir": "./documents", "allowedTypes": ["application/pdf"], "maxFileSize": 5242880 }
981
+ {
982
+ "path": "/photos",
983
+ "dir": "./photos",
984
+ "allowedTypes": ["image/jpeg", "image/png"]
985
+ },
986
+ {
987
+ "path": "/docs",
988
+ "dir": "./documents",
989
+ "allowedTypes": ["application/pdf"],
990
+ "maxFileSize": 5242880
991
+ }
979
992
  ]
980
993
  }
981
994
  ```
982
995
 
983
996
  **HTTP interface:**
984
997
 
985
- | Method | URL | Description |
986
- | ------ | ---------------- | ---------------------------------------- |
987
- | `POST` | `<path>` | Upload files via `multipart/form-data` |
988
- | `GET` | `<path>/<name>` | Retrieve a previously uploaded file |
998
+ | Method | URL | Description |
999
+ | ------ | --------------- | -------------------------------------- |
1000
+ | `POST` | `<path>` | Upload files via `multipart/form-data` |
1001
+ | `GET` | `<path>/<name>` | Retrieve a previously uploaded file |
989
1002
 
990
1003
  `POST` success response (`200`):
991
1004
 
992
1005
  ```json
993
1006
  {
994
1007
  "files": [
995
- { "file": "photo-1700000000000-123456789.jpg", "size": 45678, "originalName": "photo.jpg" }
1008
+ {
1009
+ "file": "photo-1700000000000-123456789.jpg",
1010
+ "size": 45678,
1011
+ "originalName": "photo.jpg"
1012
+ }
996
1013
  ]
997
1014
  }
998
1015
  ```
@@ -1186,9 +1203,9 @@ Redirect old URLs to new ones after a site restructure, without breaking existin
1186
1203
  {
1187
1204
  "port": 8080,
1188
1205
  "redirects": [
1189
- { "from": "/about.html", "to": "/about", "status": 301 },
1190
- { "from": "/products.html", "to": "/products", "status": 301 },
1191
- { "from": "/blog/:slug", "to": "/posts/:slug", "status": 301 }
1206
+ { "from": "/about.html", "to": "/about", "status": 301 },
1207
+ { "from": "/products.html", "to": "/products", "status": 301 },
1208
+ { "from": "/blog/:slug", "to": "/posts/:slug", "status": 301 }
1192
1209
  ],
1193
1210
  "folders": "./public"
1194
1211
  }
@@ -1199,7 +1216,7 @@ Or as an object map for simple path-to-path redirects:
1199
1216
  ```json
1200
1217
  {
1201
1218
  "redirects": {
1202
- "/old-home": "/",
1219
+ "/old-home": "/",
1203
1220
  "/old-about": "/about",
1204
1221
  "/legacy-api": "https://api.example.com"
1205
1222
  }
@@ -1386,14 +1403,14 @@ express-reverse-proxy --cluster start --cluster-config ./my-ecosystem.config.cjs
1386
1403
 
1387
1404
  Run via npm scripts:
1388
1405
 
1389
- | Script | Description |
1390
- | ----------------------- | ------------------------------------------------------------------ |
1391
- | `npm run pm2-start` | Start cluster (max CPU cores); reads `server-config.json` from cwd |
1392
- | `npm run pm2-restart` | Restart all instances |
1393
- | `npm run pm2-stop` | Stop all instances |
1394
- | `npm run pm2-status` | Show process status |
1395
- | `npm run pm2-logs` | Show last 200 log lines |
1396
- | `npm run pm2-monitor` | Open real-time monitor |
1406
+ | Script | Description |
1407
+ | --------------------- | ------------------------------------------------------------------ |
1408
+ | `npm run pm2-start` | Start cluster (max CPU cores); reads `server-config.json` from cwd |
1409
+ | `npm run pm2-restart` | Restart all instances |
1410
+ | `npm run pm2-stop` | Stop all instances |
1411
+ | `npm run pm2-status` | Show process status |
1412
+ | `npm run pm2-logs` | Show last 200 log lines |
1413
+ | `npm run pm2-monitor` | Open real-time monitor |
1397
1414
 
1398
1415
  Or use the CLI directly:
1399
1416
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lopatnov/express-reverse-proxy",
3
3
  "email": "oleksandr@lopatnov.cv.ua",
4
- "version": "5.0.6",
4
+ "version": "5.0.8",
5
5
  "description": "Node.js CLI tool to serve static front-end files with a reverse proxy for back-end APIs",
6
6
  "type": "module",
7
7
  "author": "lopatnov",
package/server.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn, spawnSync } from 'node:child_process';
3
+ import { randomInt } from 'node:crypto';
3
4
  import fs from 'node:fs';
4
5
  import https from 'node:https';
5
6
  import path from 'node:path';
@@ -316,7 +317,15 @@ function addMappedProxy(router, port, localRootPath, pathPairs) {
316
317
 
317
318
  function addProxies(router, port, localRootPath, proxies) {
318
319
  proxies.forEach((proxyUrl) => {
319
- addRemoteProxy(router, port, localRootPath, proxyUrl);
320
+ if (
321
+ Array.isArray(proxyUrl) &&
322
+ proxyUrl.length > 0 &&
323
+ proxyUrl.every((i) => typeof i === 'string')
324
+ ) {
325
+ addRemoteProxy(router, port, localRootPath, proxyUrl);
326
+ } else {
327
+ addProxy(router, port, localRootPath, proxyUrl);
328
+ }
320
329
  });
321
330
  }
322
331
 
@@ -535,7 +544,7 @@ function setupUpload(router, siteConfig, configDir) {
535
544
  const ext = path.extname(file.originalname);
536
545
  const base =
537
546
  path.basename(file.originalname, ext).replaceAll(/[^a-zA-Z0-9_.-]/g, '_') || 'file';
538
- cb(null, `${base}-${Date.now()}-${Math.round(Math.random() * 1e9)}${ext}`);
547
+ cb(null, `${base}-${Date.now()}-${randomInt(0, 1_000_000_000)}${ext}`);
539
548
  },
540
549
  });
541
550
 
@@ -548,7 +557,19 @@ function setupUpload(router, siteConfig, configDir) {
548
557
  ? uploader.array(uploadConfig.fieldName)
549
558
  : uploader.any();
550
559
 
551
- router.post(uploadUrlPath, multerMiddleware, handleUploadResponse);
560
+ router.post(uploadUrlPath, (req, res, next) => {
561
+ multerMiddleware(req, res, (err) => {
562
+ if (err instanceof multer.MulterError) {
563
+ const status = err.code === 'LIMIT_FILE_SIZE' ? 413 : 400;
564
+ return res.status(status).json({ error: err.message });
565
+ }
566
+ if (err?.status || err?.statusCode) {
567
+ return res.status(err.statusCode || err.status).json({ error: err.message });
568
+ }
569
+ if (err) return next(err);
570
+ return handleUploadResponse(req, res);
571
+ });
572
+ });
552
573
  router.use(uploadUrlPath, express.static(uploadDir));
553
574
  console.log(`[upload] POST ${uploadUrlPath} → ${uploadDir}`);
554
575
  }