hapi-terminator 0.3.1 → 0.5.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.
Files changed (4) hide show
  1. package/README.md +37 -36
  2. package/index.d.ts +4 -19
  3. package/index.js +34 -52
  4. package/package.json +1 -4
package/README.md CHANGED
@@ -36,7 +36,6 @@ const server = Hapi.server({ port: 3000, host: '127.0.0.1' });
36
36
 
37
37
  const requestTerminateOptions: TerminatorOptions = {
38
38
  unregisteredLimit: 500 * 1024, // 500KB for unregistered routes
39
- registeredLimit: 500 * 1024, // 500KB for registered routes
40
39
  };
41
40
 
42
41
  await server.register({
@@ -45,9 +44,14 @@ await server.register({
45
44
  });
46
45
 
47
46
  server.route({
48
- method: ['GET', 'POST'],
47
+ method: ['POST'],
49
48
  path: '/',
50
49
  handler: () => 'Hello World!',
50
+ options: {
51
+ payload: {
52
+ maxBytes: 500 * 1024, // 500KB limit for this route
53
+ },
54
+ },
51
55
  });
52
56
 
53
57
  await server.start();
@@ -56,25 +60,21 @@ console.log('Server running on %s', server.info.uri);
56
60
 
57
61
  ### Per-Route Limits
58
62
 
59
- You can override the global limits for specific routes by setting the limit in the route options:
63
+ You can set limits for specific routes using Hapi's native `payload.maxBytes` configuration:
60
64
 
61
65
  ```typescript
62
- import Hapi, { type PluginSpecificConfiguration } from '@hapi/hapi';
63
- import terminatorPlugin, { type TerminatorOptions, type TerminatorRouteOptions } from 'hapi-terminator';
64
-
65
- type RoutePluginOptions = PluginSpecificConfiguration & TerminatorRouteOptions;
66
+ import Hapi from '@hapi/hapi';
67
+ import terminatorPlugin, { type TerminatorOptions } from 'hapi-terminator';
66
68
 
67
69
  const server = Hapi.server({ port: 3000, host: '127.0.0.1' });
68
70
 
69
71
  await server.register({
70
72
  plugin: terminatorPlugin,
71
73
  options: {
72
- registeredLimit: 500 * 1024, // 500KB default for registered routes
73
74
  unregisteredLimit: 100 * 1024, // 100KB for unregistered routes
74
75
  },
75
76
  });
76
77
 
77
- // Standard route with default limit (500KB)
78
78
  server.route({
79
79
  method: ['GET', 'POST'],
80
80
  path: '/',
@@ -87,21 +87,9 @@ server.route({
87
87
  path: '/upload',
88
88
  handler: () => ({ success: true }),
89
89
  options: {
90
- plugins: {
91
- 'hapi-terminator': { limit: 10 * 1024 * 1024 }, // 10MB
92
- } as RoutePluginOptions,
93
- },
94
- });
95
-
96
- // Unlimited route (disable limit for this specific route)
97
- server.route({
98
- method: ['POST'],
99
- path: '/stream',
100
- handler: () => ({ success: true }),
101
- options: {
102
- plugins: {
103
- 'hapi-terminator': { limit: null }, // No limit
104
- } as RoutePluginOptions,
90
+ payload: {
91
+ maxBytes: 10 * 1024 * 1024, // 10MB
92
+ },
105
93
  },
106
94
  });
107
95
 
@@ -122,7 +110,6 @@ const server = Hapi.server({ port: 3000, host: '127.0.0.1' });
122
110
  await server.register({
123
111
  plugin: terminatorPlugin,
124
112
  options: {
125
- registeredLimit: 1024 * 1024, // 1MB for registered routes
126
113
  unregisteredLimit: true, // Immediately reject all unregistered routes
127
114
  },
128
115
  });
@@ -132,6 +119,11 @@ server.route({
132
119
  method: ['POST'],
133
120
  path: '/api/data',
134
121
  handler: () => ({ success: true }),
122
+ options: {
123
+ payload: {
124
+ maxBytes: 1024 * 1024, // 1MB
125
+ },
126
+ },
135
127
  });
136
128
 
137
129
  // Any request to unregistered routes (e.g., /unknown) will be rejected immediately
@@ -144,7 +136,6 @@ You can also set `unregisteredLimit` to `false` to bypass payload size checks fo
144
136
  await server.register({
145
137
  plugin: terminatorPlugin,
146
138
  options: {
147
- registeredLimit: 500 * 1024, // 500KB for registered routes
148
139
  unregisteredLimit: false, // Bypass payload size checks for unregistered routes
149
140
  },
150
141
  });
@@ -156,23 +147,33 @@ await server.register({
156
147
 
157
148
  | Option | Type | Description |
158
149
  | ------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
159
- | `registeredLimit` | `number` | Maximum payload size in bytes for registered routes. Must be >= 0. Set to `null` or `undefined` to disable. |
160
150
  | `unregisteredLimit` | `number \| boolean` | Maximum payload size in bytes for unregistered routes. Must be >= 0. Set to `null` or `undefined` to disable. Set to `true` to reject all requests immediately. Set to `false` to bypass payload size checks. |
161
151
 
162
- ### TerminatorRouteOptions
152
+ ### Route Payload Configuration
163
153
 
164
- You can configure per-route limits using the route options:
154
+ Use Hapi's native `payload.maxBytes` option in your route configuration to set per-route limits:
165
155
 
166
- | Option | Type | Description |
167
- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------- |
168
- | `limit` | `number` | Maximum payload size in bytes for this specific route. Must be >= 0. Overrides the global `registeredLimit`. Set to `null` to disable. |
156
+ Use Hapi's native `payload.maxBytes` option in your route configuration to set per-route limits:
157
+
158
+ ```typescript
159
+ server.route({
160
+ method: 'POST',
161
+ path: '/upload',
162
+ handler: () => ({ success: true }),
163
+ options: {
164
+ payload: {
165
+ maxBytes: 10 * 1024 * 1024, // 10MB
166
+ },
167
+ },
168
+ });
169
+ ```
169
170
 
170
171
  ### Behavior
171
172
 
172
- - **Registered Routes**: When a payload exceeds the limit on a registered route, the socket is gracefully ended and a `413 Payload Too Large` error is returned.
173
- - **Unregistered Routes**: When a payload exceeds the limit on an unregistered route, the socket is gracefully ended and a `404 Not Found` error is returned.
174
- - **Per-Route Limits**: Route-specific limits take precedence over global limits, allowing you to customize limits for individual routes.
175
- - **Disabled**: Set to `null` or `undefined` to disable termination for that category or route.
173
+ - **Registered Routes**: Routes use Hapi's native `payload.maxBytes` setting. When a payload exceeds this limit, the socket is gracefully ended and a `413 Payload Too Large` error is returned.
174
+ - **Unregistered Routes**: When a payload exceeds the `unregisteredLimit`, the socket is gracefully ended and a `404 Not Found` error is returned.
175
+ - **Per-Route Limits**: Use Hapi's `payload.maxBytes` to customize limits for individual routes.
176
+ - **Disabled**: Omit `payload.maxBytes` to allow unlimited payload size for a route.
176
177
  - **Boolean Values for Unregistered Routes**:
177
178
  - Set `unregisteredLimit` to `true` to immediately reject all unregistered route requests regardless of Content-Length (even 0 bytes).
178
179
  - Set `unregisteredLimit` to `false` to bypass payload size checks for unregistered route requests (they will still receive 404 responses).
package/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { Server } from '@hapi/hapi';
2
- import { z } from 'zod/mini';
3
- export type TerminatorRouteOptions = z.infer<typeof TerminatorRouteOptionsSchema>;
4
- export type TerminatorOptions = z.infer<typeof TerminatorOptionsSchema>;
2
+ export type TerminatorOptions = {
3
+ unregisteredLimit?: number | boolean | null;
4
+ };
5
5
  export declare const plugin: {
6
6
  pkg: {
7
7
  name: string;
@@ -27,9 +27,6 @@ export declare const plugin: {
27
27
  typescript: string;
28
28
  "typescript-eslint": string;
29
29
  };
30
- dependencies: {
31
- zod: string;
32
- };
33
30
  prettier: string;
34
31
  "lint-staged": {
35
32
  "**/*.{js,ts,tsx}": string[];
@@ -53,16 +50,7 @@ export declare const plugin: {
53
50
  };
54
51
  register: typeof register;
55
52
  };
56
- declare const TerminatorRouteOptionsSchema: z.ZodMiniOptional<z.ZodMiniNullable<z.ZodMiniObject<{
57
- "hapi-terminator": z.ZodMiniObject<{
58
- limit: z.ZodMiniOptional<z.ZodMiniNullable<z.ZodMiniNumber<number>>>;
59
- }, z.core.$strip>;
60
- }, z.core.$strip>>>;
61
- declare const TerminatorOptionsSchema: z.ZodMiniOptional<z.ZodMiniNullable<z.ZodMiniObject<{
62
- registeredLimit: z.ZodMiniOptional<z.ZodMiniNullable<z.ZodMiniNumber<number>>>;
63
- unregisteredLimit: z.ZodMiniUnion<readonly [z.ZodMiniOptional<z.ZodMiniNullable<z.ZodMiniNumber<number>>>, z.ZodMiniBoolean<boolean>]>;
64
- }, z.core.$strip>>>;
65
- declare function register(server: Server, rawOptions: TerminatorOptions): Promise<void>;
53
+ declare function register(server: Server, rawOptions: Record<string, unknown> | null | undefined): Promise<void>;
66
54
  declare const _default: {
67
55
  plugin: {
68
56
  pkg: {
@@ -89,9 +77,6 @@ declare const _default: {
89
77
  typescript: string;
90
78
  "typescript-eslint": string;
91
79
  };
92
- dependencies: {
93
- zod: string;
94
- };
95
80
  prettier: string;
96
81
  "lint-staged": {
97
82
  "**/*.{js,ts,tsx}": string[];
package/index.js CHANGED
@@ -1,37 +1,23 @@
1
- import assert from 'node:assert';
2
- import { z } from 'zod/mini';
3
1
  import pkg from './package.json';
4
- const LIMIT_OPTION_NAMES = {
5
- REGISTERED: 'registeredLimit',
6
- UNREGISTERED: 'unregisteredLimit',
7
- };
8
- const PACKAGE_NAME = 'hapi-terminator';
9
- assert(PACKAGE_NAME === pkg.name);
10
2
  export const plugin = { pkg, register };
11
- const TerminatorRouteOptionsSchema = z.nullish(z.object({ [PACKAGE_NAME]: z.object({ limit: z.nullish(z.number().check(z.minimum(0))) }) }));
12
- const TerminatorOptionsSchema = z.nullish(z.object({
13
- registeredLimit: z.nullish(z.number().check(z.minimum(0))),
14
- unregisteredLimit: z.union([z.nullish(z.number().check(z.minimum(0))), z.boolean()]),
15
- }));
16
3
  async function register(server, rawOptions) {
17
- const options = TerminatorOptionsSchema.parse(rawOptions);
18
- const routeOptionsCache = new Map();
19
- server.ext('onRequest', validateHookHandler(options, routeOptionsCache));
4
+ const options = validateOptions(rawOptions);
5
+ server.ext('onRequest', validateHookHandler(options));
20
6
  }
21
- function validateHookHandler(pluginOptions, routeOptionsCache) {
7
+ function validateHookHandler(pluginOptions) {
22
8
  return (request, h) => {
23
- const handler = validateRoute(request, h, pluginOptions);
9
+ const handler = validateRoute(request, h);
10
+ const hasTransferEncoding = Boolean(request.headers['transfer-encoding']);
24
11
  const rawContentLength = request.headers['content-length'];
25
- if (!rawContentLength) {
12
+ const willProcessPayload = hasTransferEncoding || Boolean(rawContentLength);
13
+ if (!willProcessPayload) {
26
14
  return h.continue;
27
15
  }
28
- const contentLength = Number.parseInt(rawContentLength, 10);
29
- if (Number.isNaN(contentLength)) {
30
- return h.continue;
31
- }
32
- const { route, options } = getRouteAndOptions(request, routeOptionsCache) ?? {};
16
+ const contentLength = Number.parseInt(rawContentLength || '0', 10);
17
+ const route = request.server.match(request.method, request.path, request.info.host);
33
18
  if (route != null) {
34
- return handler(contentLength, LIMIT_OPTION_NAMES.REGISTERED, options, option => {
19
+ const maxBytes = route.settings.payload?.maxBytes;
20
+ return handler(contentLength, maxBytes, option => {
35
21
  return h
36
22
  .response({
37
23
  error: 'Request Entity Too Large',
@@ -41,30 +27,13 @@ function validateHookHandler(pluginOptions, routeOptionsCache) {
41
27
  .code(413);
42
28
  });
43
29
  }
44
- assert(options == null, "Unregistered routes can't have route options");
45
- return handler(contentLength, LIMIT_OPTION_NAMES.UNREGISTERED, null, () => {
30
+ return handler(contentLength, (hasTransferEncoding ? 0 : null) ?? pluginOptions?.unregisteredLimit, () => {
46
31
  return h.response({ error: 'Not Found', message: 'Not Found', statusCode: 404 }).code(404);
47
32
  });
48
33
  };
49
34
  }
50
- function getRouteAndOptions(request, routeOptionsCache) {
51
- const matchedRoute = request.server.match(request.method, request.path, request.info.host);
52
- if (matchedRoute == null) {
53
- return null;
54
- }
55
- const cacheKey = `${matchedRoute.method}-${matchedRoute.path}`;
56
- const cachedResult = routeOptionsCache.get(cacheKey);
57
- if (cachedResult != null) {
58
- return { options: cachedResult, route: matchedRoute };
59
- }
60
- const options = getRoutePluginSettings(matchedRoute);
61
- routeOptionsCache.set(cacheKey, options);
62
- return { options, route: matchedRoute };
63
- }
64
- function validateRoute(request, h, options) {
65
- return (contentLength, optionName, routeOptions, response) => {
66
- const option = options?.[optionName];
67
- const limit = routeOptions?.[PACKAGE_NAME]?.limit ?? option;
35
+ function validateRoute(request, h) {
36
+ return (contentLength, limit, response) => {
68
37
  if (limit == null) {
69
38
  return h.continue;
70
39
  }
@@ -76,12 +45,12 @@ function validateRoute(request, h, options) {
76
45
  closeSocketsOnFinish(request);
77
46
  return result;
78
47
  }
79
- if (contentLength <= limit) {
80
- return h.continue;
48
+ if (limit === 0 || contentLength > limit) {
49
+ const result = response(limit).takeover();
50
+ closeSocketsOnFinish(request);
51
+ return result;
81
52
  }
82
- const result = response(limit).takeover();
83
- closeSocketsOnFinish(request);
84
- return result;
53
+ return h.continue;
85
54
  };
86
55
  }
87
56
  function closeSocketsOnFinish(request) {
@@ -95,7 +64,20 @@ function closeSocketsOnFinish(request) {
95
64
  }
96
65
  });
97
66
  }
98
- function getRoutePluginSettings(matchedRoute) {
99
- return TerminatorRouteOptionsSchema.safeParse(matchedRoute?.settings.plugins).data;
67
+ function validateOptions(options) {
68
+ if (options == null) {
69
+ return { unregisteredLimit: null };
70
+ }
71
+ if (!('unregisteredLimit' in options)) {
72
+ return { unregisteredLimit: null };
73
+ }
74
+ const unregisteredLimit = options.unregisteredLimit;
75
+ if (typeof unregisteredLimit === 'number') {
76
+ return { unregisteredLimit: Math.max(0, unregisteredLimit) };
77
+ }
78
+ if (typeof unregisteredLimit === 'boolean') {
79
+ return { unregisteredLimit };
80
+ }
81
+ return { unregisteredLimit: null };
100
82
  }
101
83
  export default { plugin };
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "main": "index.js",
4
4
  "typings": "index.d.ts",
5
5
  "type": "module",
6
- "version": "0.3.1",
6
+ "version": "0.5.0",
7
7
  "license": "MIT",
8
8
  "author": {
9
9
  "name": "Kamaal Farah"
@@ -22,9 +22,6 @@
22
22
  "typescript": "^5.9.3",
23
23
  "typescript-eslint": "^8.52.0"
24
24
  },
25
- "dependencies": {
26
- "zod": "^4.3.5"
27
- },
28
25
  "prettier": "@kamaalio/prettier-config",
29
26
  "lint-staged": {
30
27
  "**/*.{js,ts,tsx}": [