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.
- package/README.md +37 -36
- package/index.d.ts +4 -19
- package/index.js +34 -52
- 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: ['
|
|
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
|
|
63
|
+
You can set limits for specific routes using Hapi's native `payload.maxBytes` configuration:
|
|
60
64
|
|
|
61
65
|
```typescript
|
|
62
|
-
import Hapi
|
|
63
|
-
import terminatorPlugin, { type TerminatorOptions
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
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
|
-
###
|
|
152
|
+
### Route Payload Configuration
|
|
163
153
|
|
|
164
|
-
|
|
154
|
+
Use Hapi's native `payload.maxBytes` option in your route configuration to set per-route limits:
|
|
165
155
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
173
|
-
- **Unregistered Routes**: When a payload exceeds the
|
|
174
|
-
- **Per-Route Limits**:
|
|
175
|
-
- **Disabled**:
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
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 =
|
|
18
|
-
|
|
19
|
-
server.ext('onRequest', validateHookHandler(options, routeOptionsCache));
|
|
4
|
+
const options = validateOptions(rawOptions);
|
|
5
|
+
server.ext('onRequest', validateHookHandler(options));
|
|
20
6
|
}
|
|
21
|
-
function validateHookHandler(pluginOptions
|
|
7
|
+
function validateHookHandler(pluginOptions) {
|
|
22
8
|
return (request, h) => {
|
|
23
|
-
const handler = validateRoute(request, h
|
|
9
|
+
const handler = validateRoute(request, h);
|
|
10
|
+
const hasTransferEncoding = Boolean(request.headers['transfer-encoding']);
|
|
24
11
|
const rawContentLength = request.headers['content-length'];
|
|
25
|
-
|
|
12
|
+
const willProcessPayload = hasTransferEncoding || Boolean(rawContentLength);
|
|
13
|
+
if (!willProcessPayload) {
|
|
26
14
|
return h.continue;
|
|
27
15
|
}
|
|
28
|
-
const contentLength = Number.parseInt(rawContentLength, 10);
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
51
|
-
|
|
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
|
|
80
|
-
|
|
48
|
+
if (limit === 0 || contentLength > limit) {
|
|
49
|
+
const result = response(limit).takeover();
|
|
50
|
+
closeSocketsOnFinish(request);
|
|
51
|
+
return result;
|
|
81
52
|
}
|
|
82
|
-
|
|
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
|
|
99
|
-
|
|
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.
|
|
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}": [
|