woodland 22.1.0 → 22.2.0

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.
package/README.md CHANGED
@@ -155,6 +155,22 @@ app.use((error, req, res, next) => {
155
155
  });
156
156
  ```
157
157
 
158
+ ### Global Error Handling
159
+
160
+ Set `app.error` to intercept all unhandled errors before the error middleware chain:
161
+
162
+ ```javascript
163
+ const app = woodland();
164
+
165
+ app.error = (err, _req, res) => {
166
+ console.error("Unhandled error:", err);
167
+ res.status(500).send("Internal server error");
168
+ };
169
+ ```
170
+
171
+ The handler receives 3 arguments `(err, req, res)` and must terminate the request itself. When set, the error middleware chain is skipped.
172
+
173
+
158
174
  ## Configuration
159
175
 
160
176
  ```javascript
@@ -196,6 +212,7 @@ req.cors; // CORS enabled
196
212
  req.body; // Request body
197
213
  req.host; // Hostname
198
214
  req.valid; // Request validity
215
+ req.app; // Woodland application instance (provides access to app.error)
199
216
  ```
200
217
 
201
218
  ## Event Handlers
package/dist/cli.cjs CHANGED
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * @copyright 2026 Jason Mulligan <jason.mulligan@avoidwork.com>
6
6
  * @license BSD-3-Clause
7
- * @version 22.1.0
7
+ * @version 22.2.0
8
8
  */
9
9
  'use strict';
10
10
 
package/dist/woodland.cjs CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * @copyright 2026 Jason Mulligan <jason.mulligan@avoidwork.com>
5
5
  * @license BSD-3-Clause
6
- * @version 22.1.0
6
+ * @version 22.2.0
7
7
  */
8
8
  'use strict';
9
9
 
@@ -110,7 +110,6 @@ const INT_60 = 60;
110
110
  const INT_255 = 255;
111
111
  const INT_1e3 = 1e3;
112
112
  const INT_1e4 = 1e4;
113
- const INT_1e6$1 = 1e6;
114
113
  const INT_8000 = 8000;
115
114
  const INT_NEG_1 = -1;
116
115
 
@@ -166,6 +165,7 @@ const NEWLINE = "\n";
166
165
  const ROUTE_PATTERN = "(/.*)?";
167
166
  const MSG_USE_MIDDLEWARE_REQUIRED =
168
167
  "useMiddleware is required or config.use must be a function";
168
+ const MSG_MIDDLEWARE_REQUIRED = "Expected a function in the parameters";
169
169
  const EXTRACT_PATH_REPLACE = "(?<$1>[^/]+)";
170
170
  const TPL_DIR = "tpl";
171
171
  const INDEX_HTML_FILE = "index.html";
@@ -1164,6 +1164,10 @@ function next(req, res, middleware, immediate = false) {
1164
1164
  */
1165
1165
  const execute = (err) => {
1166
1166
  if (err !== void 0) {
1167
+ if (typeof req.app?.error === FUNCTION) {
1168
+ req.app.error(err, req, res);
1169
+ return;
1170
+ }
1167
1171
  handleError(err, execute);
1168
1172
  } else {
1169
1173
  handleMiddleware(execute);
@@ -1357,7 +1361,7 @@ function registerMiddleware(middleware, ignored, methods, rpath, ...fn) {
1357
1361
 
1358
1362
  const DEFAULTS = {
1359
1363
  autoIndex: false,
1360
- bodyLimit: INT_10 * INT_1e6$1,
1364
+ bodyLimit: INT_10 * 1e6,
1361
1365
  cacheSize: INT_1e3,
1362
1366
  cacheTTL: INT_1e4,
1363
1367
  charset: UTF_8,
@@ -1747,6 +1751,7 @@ async function serve(config, req, res, arg, folder = process.cwd()) {
1747
1751
 
1748
1752
  if (!valid) {
1749
1753
  res.error(INT_404, new Error(node_http.STATUS_CODES[INT_404]));
1754
+ return;
1750
1755
  } else if (!stats.isDirectory()) {
1751
1756
  config.stream(req, res, {
1752
1757
  charset: config.charset,
@@ -1754,8 +1759,10 @@ async function serve(config, req, res, arg, folder = process.cwd()) {
1754
1759
  path: realFp,
1755
1760
  stats: stats,
1756
1761
  });
1762
+ return;
1757
1763
  } else if (!req.parsed.pathname.endsWith(SLASH)) {
1758
1764
  res.redirect(`${req.parsed.pathname}${SLASH}${req.parsed.search}`);
1765
+ return;
1759
1766
  } else {
1760
1767
  let files;
1761
1768
  /* node:coverage ignore next 7 */
@@ -1780,13 +1787,16 @@ async function serve(config, req, res, arg, folder = process.cwd()) {
1780
1787
  if (!result.length) {
1781
1788
  if (!config.autoIndex) {
1782
1789
  res.error(INT_404, new Error(node_http.STATUS_CODES[INT_404]));
1790
+ return;
1783
1791
  } else {
1784
1792
  try {
1785
1793
  const body = autoIndex(decodeURIComponent(req.parsed.pathname), files);
1786
1794
  res.header(CONTENT_TYPE, `${TEXT_HTML}; charset=${config.charset}`);
1787
1795
  res.send(body);
1796
+ return;
1788
1797
  } catch {
1789
1798
  res.error(INT_400, new Error(node_http.STATUS_CODES[INT_400]));
1799
+ return;
1790
1800
  }
1791
1801
  }
1792
1802
  } else {
@@ -1805,6 +1815,7 @@ async function serve(config, req, res, arg, folder = process.cwd()) {
1805
1815
  path: result,
1806
1816
  stats: rstats,
1807
1817
  });
1818
+ return;
1808
1819
  }
1809
1820
  }
1810
1821
  }
@@ -1878,6 +1889,7 @@ class Woodland extends node_events.EventEmitter {
1878
1889
  #logger;
1879
1890
  #fileServer;
1880
1891
  #middleware;
1892
+ #error;
1881
1893
 
1882
1894
  /**
1883
1895
  * Creates a new Woodland instance
@@ -1924,6 +1936,7 @@ class Woodland extends node_events.EventEmitter {
1924
1936
  this.#logger = this.#createLogger();
1925
1937
  this.#fileServer = this.#createFileServer();
1926
1938
  this.#middleware = createMiddlewareRegistry(this.#methods, this.#cache);
1939
+ this.#error = null;
1927
1940
 
1928
1941
  this.#setupMiddleware();
1929
1942
  this.#setupErrorHandling();
@@ -2097,6 +2110,10 @@ class Woodland extends node_events.EventEmitter {
2097
2110
  * @returns {Woodland} Returns self for chaining
2098
2111
  */
2099
2112
  #registerMethod(method, ...args) {
2113
+ if (args.length === INT_1 && typeof args[INT_0] === STRING) {
2114
+ throw new TypeError(MSG_MIDDLEWARE_REQUIRED);
2115
+ }
2116
+
2100
2117
  return this.use(...args, method);
2101
2118
  }
2102
2119
 
@@ -2127,6 +2144,7 @@ class Woodland extends node_events.EventEmitter {
2127
2144
  req.host = parsed.hostname;
2128
2145
  req.params = {};
2129
2146
  req.valid = true;
2147
+ req.app = this;
2130
2148
 
2131
2149
  const allowString = this.#allows(parsed.pathname);
2132
2150
  const headersBatch = Object.create(null);
@@ -2340,7 +2358,7 @@ class Woodland extends node_events.EventEmitter {
2340
2358
  /* node:coverage ignore next 5 */
2341
2359
  if (this.#time && res.getHeader(X_RESPONSE_TIME) === void 0) {
2342
2360
  const diff = req.precise.stop().diff();
2343
- const msValue = Number(diff / INT_1e6).toFixed(this.#digit);
2361
+ const msValue = Number(diff / 1e6).toFixed(this.#digit);
2344
2362
  res.header(X_RESPONSE_TIME, `${msValue}${RESPONSE_TIME_UNIT}`);
2345
2363
  }
2346
2364
 
@@ -2543,6 +2561,19 @@ class Woodland extends node_events.EventEmitter {
2543
2561
  get logger() {
2544
2562
  return this.#logger;
2545
2563
  }
2564
+
2565
+ /**
2566
+ * Global error handler property
2567
+ * @param {Function} [fn] - Error handler function
2568
+ * @returns {Function|null} Current error handler or null
2569
+ */
2570
+ get error() {
2571
+ return this.#error;
2572
+ }
2573
+
2574
+ set error(fn) {
2575
+ this.#error = typeof fn === FUNCTION ? fn : null;
2576
+ }
2546
2577
  }
2547
2578
 
2548
2579
  /**
package/dist/woodland.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * @copyright 2026 Jason Mulligan <jason.mulligan@avoidwork.com>
5
5
  * @license BSD-3-Clause
6
- * @version 22.1.0
6
+ * @version 22.2.0
7
7
  */
8
8
  import {STATUS_CODES}from'node:http';import {EventEmitter}from'node:events';import {readFileSync,createReadStream}from'node:fs';import {etag}from'tiny-etag';import {lru}from'tiny-lru';import {precise}from'precise';import {createRequire}from'node:module';import {join,extname,resolve,sep}from'node:path';import {fileURLToPath,URL as URL$1}from'node:url';import mimeDb from'mime-db';import {coerce}from'tiny-coerce';import {Validator}from'jsonschema';import {realpath,stat,readdir}from'node:fs/promises';const __dirname$2 = fileURLToPath(new URL$1(".", import.meta.url));
9
9
  const require$1 = createRequire(import.meta.url);
@@ -93,7 +93,6 @@ const INT_60 = 60;
93
93
  const INT_255 = 255;
94
94
  const INT_1e3 = 1e3;
95
95
  const INT_1e4 = 1e4;
96
- const INT_1e6$1 = 1e6;
97
96
  const INT_8000 = 8000;
98
97
  const INT_NEG_1 = -1;
99
98
 
@@ -149,6 +148,7 @@ const NEWLINE = "\n";
149
148
  const ROUTE_PATTERN = "(/.*)?";
150
149
  const MSG_USE_MIDDLEWARE_REQUIRED =
151
150
  "useMiddleware is required or config.use must be a function";
151
+ const MSG_MIDDLEWARE_REQUIRED = "Expected a function in the parameters";
152
152
  const EXTRACT_PATH_REPLACE = "(?<$1>[^/]+)";
153
153
  const TPL_DIR = "tpl";
154
154
  const INDEX_HTML_FILE = "index.html";
@@ -1141,6 +1141,10 @@ function next(req, res, middleware, immediate = false) {
1141
1141
  */
1142
1142
  const execute = (err) => {
1143
1143
  if (err !== void 0) {
1144
+ if (typeof req.app?.error === FUNCTION) {
1145
+ req.app.error(err, req, res);
1146
+ return;
1147
+ }
1144
1148
  handleError(err, execute);
1145
1149
  } else {
1146
1150
  handleMiddleware(execute);
@@ -1332,7 +1336,7 @@ function registerMiddleware(middleware, ignored, methods, rpath, ...fn) {
1332
1336
  });
1333
1337
  }const DEFAULTS = {
1334
1338
  autoIndex: false,
1335
- bodyLimit: INT_10 * INT_1e6$1,
1339
+ bodyLimit: INT_10 * 1e6,
1336
1340
  cacheSize: INT_1e3,
1337
1341
  cacheTTL: INT_1e4,
1338
1342
  charset: UTF_8,
@@ -1718,6 +1722,7 @@ async function serve(config, req, res, arg, folder = process.cwd()) {
1718
1722
 
1719
1723
  if (!valid) {
1720
1724
  res.error(INT_404, new Error(STATUS_CODES[INT_404]));
1725
+ return;
1721
1726
  } else if (!stats.isDirectory()) {
1722
1727
  config.stream(req, res, {
1723
1728
  charset: config.charset,
@@ -1725,8 +1730,10 @@ async function serve(config, req, res, arg, folder = process.cwd()) {
1725
1730
  path: realFp,
1726
1731
  stats: stats,
1727
1732
  });
1733
+ return;
1728
1734
  } else if (!req.parsed.pathname.endsWith(SLASH)) {
1729
1735
  res.redirect(`${req.parsed.pathname}${SLASH}${req.parsed.search}`);
1736
+ return;
1730
1737
  } else {
1731
1738
  let files;
1732
1739
  /* node:coverage ignore next 7 */
@@ -1751,13 +1758,16 @@ async function serve(config, req, res, arg, folder = process.cwd()) {
1751
1758
  if (!result.length) {
1752
1759
  if (!config.autoIndex) {
1753
1760
  res.error(INT_404, new Error(STATUS_CODES[INT_404]));
1761
+ return;
1754
1762
  } else {
1755
1763
  try {
1756
1764
  const body = autoIndex(decodeURIComponent(req.parsed.pathname), files);
1757
1765
  res.header(CONTENT_TYPE, `${TEXT_HTML}; charset=${config.charset}`);
1758
1766
  res.send(body);
1767
+ return;
1759
1768
  } catch {
1760
1769
  res.error(INT_400, new Error(STATUS_CODES[INT_400]));
1770
+ return;
1761
1771
  }
1762
1772
  }
1763
1773
  } else {
@@ -1776,6 +1786,7 @@ async function serve(config, req, res, arg, folder = process.cwd()) {
1776
1786
  path: result,
1777
1787
  stats: rstats,
1778
1788
  });
1789
+ return;
1779
1790
  }
1780
1791
  }
1781
1792
  }
@@ -1847,6 +1858,7 @@ class Woodland extends EventEmitter {
1847
1858
  #logger;
1848
1859
  #fileServer;
1849
1860
  #middleware;
1861
+ #error;
1850
1862
 
1851
1863
  /**
1852
1864
  * Creates a new Woodland instance
@@ -1893,6 +1905,7 @@ class Woodland extends EventEmitter {
1893
1905
  this.#logger = this.#createLogger();
1894
1906
  this.#fileServer = this.#createFileServer();
1895
1907
  this.#middleware = createMiddlewareRegistry(this.#methods, this.#cache);
1908
+ this.#error = null;
1896
1909
 
1897
1910
  this.#setupMiddleware();
1898
1911
  this.#setupErrorHandling();
@@ -2066,6 +2079,10 @@ class Woodland extends EventEmitter {
2066
2079
  * @returns {Woodland} Returns self for chaining
2067
2080
  */
2068
2081
  #registerMethod(method, ...args) {
2082
+ if (args.length === INT_1 && typeof args[INT_0] === STRING) {
2083
+ throw new TypeError(MSG_MIDDLEWARE_REQUIRED);
2084
+ }
2085
+
2069
2086
  return this.use(...args, method);
2070
2087
  }
2071
2088
 
@@ -2096,6 +2113,7 @@ class Woodland extends EventEmitter {
2096
2113
  req.host = parsed.hostname;
2097
2114
  req.params = {};
2098
2115
  req.valid = true;
2116
+ req.app = this;
2099
2117
 
2100
2118
  const allowString = this.#allows(parsed.pathname);
2101
2119
  const headersBatch = Object.create(null);
@@ -2309,7 +2327,7 @@ class Woodland extends EventEmitter {
2309
2327
  /* node:coverage ignore next 5 */
2310
2328
  if (this.#time && res.getHeader(X_RESPONSE_TIME) === void 0) {
2311
2329
  const diff = req.precise.stop().diff();
2312
- const msValue = Number(diff / INT_1e6).toFixed(this.#digit);
2330
+ const msValue = Number(diff / 1e6).toFixed(this.#digit);
2313
2331
  res.header(X_RESPONSE_TIME, `${msValue}${RESPONSE_TIME_UNIT}`);
2314
2332
  }
2315
2333
 
@@ -2512,6 +2530,19 @@ class Woodland extends EventEmitter {
2512
2530
  get logger() {
2513
2531
  return this.#logger;
2514
2532
  }
2533
+
2534
+ /**
2535
+ * Global error handler property
2536
+ * @param {Function} [fn] - Error handler function
2537
+ * @returns {Function|null} Current error handler or null
2538
+ */
2539
+ get error() {
2540
+ return this.#error;
2541
+ }
2542
+
2543
+ set error(fn) {
2544
+ this.#error = typeof fn === FUNCTION ? fn : null;
2545
+ }
2515
2546
  }
2516
2547
 
2517
2548
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "woodland",
3
- "version": "22.1.0",
3
+ "version": "22.2.0",
4
4
  "description": "Secure HTTP framework for Node.js. Express-compatible with built-in security, no performance tradeoff.",
5
5
  "keywords": [
6
6
  "api",
@@ -73,7 +73,7 @@
73
73
  "devDependencies": {
74
74
  "auto-changelog": "^2.5.0",
75
75
  "husky": "^9.1.7",
76
- "oxfmt": "^0.46.0",
76
+ "oxfmt": "^0.49.0",
77
77
  "oxlint": "^1.57.0",
78
78
  "rimraf": "^6.1.3",
79
79
  "rollup": "^4.60.0"
@@ -49,6 +49,9 @@ export class Woodland extends EventEmitter {
49
49
  clf: (...args: any[]) => string;
50
50
  }>;
51
51
 
52
+ // Public error handler property (getter/setter)
53
+ error: ((err: Error, req: any, res: any) => void) | null;
54
+
52
55
  constructor(config?: WoodlandConfig);
53
56
 
54
57
  // Public routing methods - with path