lightpress 1.1.0 → 2.0.0-beta.1
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/LICENSE +0 -0
- package/README.md +75 -139
- package/lib/create-request-listener.d.ts +16 -0
- package/lib/create-request-listener.js +27 -0
- package/lib/http-error.d.ts +10 -13
- package/lib/http-error.js +21 -0
- package/lib/index.d.ts +2 -9
- package/lib/index.js +3 -143
- package/lib/is-readable-stream.d.ts +0 -1
- package/lib/is-readable-stream.js +6 -0
- package/lib/send-result.d.ts +4 -5
- package/lib/send-result.js +17 -0
- package/lib/utility/assert-content-type.d.ts +6 -0
- package/lib/utility/assert-content-type.js +6 -0
- package/lib/utility/assert-method.d.ts +4 -0
- package/lib/utility/assert-method.js +6 -0
- package/lib/utility/consume-body.d.ts +2 -0
- package/lib/utility/consume-body.js +15 -0
- package/lib/utility/consume-json-body.d.ts +2 -0
- package/lib/utility/consume-json-body.js +13 -0
- package/lib/utility/index.d.ts +4 -0
- package/lib/utility/index.js +4 -0
- package/package.json +52 -52
- package/lib/from-lightpress-error.d.ts +0 -8
- package/lib/index.mjs +0 -136
- package/lib/is-lightpress-error.d.ts +0 -6
- package/lib/lightpress.d.ts +0 -10
- package/lib/types/lightpress-context.d.ts +0 -7
- package/lib/types/lightpress-error.d.ts +0 -11
- package/lib/types/lightpress-handler.d.ts +0 -7
- package/lib/types/lightpress-recovery-handler.d.ts +0 -5
- package/lib/types/lightpress-result.d.ts +0 -11
package/LICENSE
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
@@ -1,196 +1,132 @@
|
|
|
1
1
|
# lightpress
|
|
2
2
|
|
|
3
|
-
Lightpress is a thin wrapper around
|
|
4
|
-
you to
|
|
5
|
-
|
|
6
|
-
- compose a handler tree without overhead
|
|
7
|
-
- write reusable and easy-to-test handler functions
|
|
8
|
-
|
|
9
|
-
Although you can use lightpress for any kind of application, it was designed
|
|
10
|
-
with modern API driven web applications in mind. These usually require a single
|
|
11
|
-
handler for serving the (SSR) HTML content, another one for static assets, and
|
|
12
|
-
one or more handlers for data.
|
|
3
|
+
Lightpress is a thin wrapper around Node's HTTP request event, providing a composable HTTP handler interface.
|
|
13
4
|
|
|
14
5
|
## Installation
|
|
15
6
|
|
|
16
|
-
You can install lightpress from [npmjs.com](https://www.npmjs.com) using your
|
|
17
|
-
favorite package manager, e.g.
|
|
18
|
-
|
|
19
7
|
```bash
|
|
20
|
-
|
|
8
|
+
npm add lightpress
|
|
21
9
|
```
|
|
22
10
|
|
|
23
|
-
##
|
|
24
|
-
|
|
25
|
-
In lightpress a request handler is a plain function that takes a context object
|
|
26
|
-
as single argument and returns a result or a promise that resolves to a result.
|
|
27
|
-
|
|
28
|
-
By default, the context object only contains a reference to the incoming
|
|
29
|
-
request, but can be augmented to your application's needs.
|
|
11
|
+
## Basic Usage
|
|
30
12
|
|
|
31
|
-
|
|
32
|
-
`statusCode`, `headers` and a `body`.
|
|
13
|
+
Create an HTTP handler function that receives a Node.js `IncomingMessage` and returns a result:
|
|
33
14
|
|
|
34
15
|
```js
|
|
35
16
|
import { createServer } from "http";
|
|
36
|
-
import
|
|
17
|
+
import { createRequestListener } from "lightpress";
|
|
37
18
|
|
|
38
|
-
function
|
|
19
|
+
function greet(request) {
|
|
39
20
|
return {
|
|
40
21
|
statusCode: 200,
|
|
41
|
-
headers: {
|
|
42
|
-
|
|
43
|
-
},
|
|
44
|
-
body: `Hello from '${context.request.url}'.`,
|
|
22
|
+
headers: { "Content-Type": "text/plain" },
|
|
23
|
+
body: `Hello from '${request.url}'.`,
|
|
45
24
|
};
|
|
46
25
|
}
|
|
47
26
|
|
|
48
|
-
|
|
49
|
-
|
|
27
|
+
createServer(
|
|
28
|
+
createRequestListener(greet)
|
|
29
|
+
).listen(8080);
|
|
50
30
|
```
|
|
51
31
|
|
|
52
32
|
## Composing Handlers
|
|
53
33
|
|
|
54
|
-
|
|
55
|
-
lightpress solves this circumstance is by composing its handlers.
|
|
56
|
-
|
|
57
|
-
Lets imagine, the `hello` handler from above must only be called for `GET`
|
|
58
|
-
requests. To achieve this we could simply check the request method inside our
|
|
59
|
-
`hello` handler. However, a better approach is to create a separate
|
|
60
|
-
handler which only cares about request methods.
|
|
34
|
+
You can compose handlers for more control. For example, you can restrict allowed HTTP methods.
|
|
61
35
|
|
|
62
36
|
```js
|
|
63
|
-
import
|
|
64
|
-
|
|
65
|
-
// ...
|
|
37
|
+
import { HttpError } from "lightpress";
|
|
66
38
|
|
|
67
39
|
function allowedMethods(methods, handler) {
|
|
68
|
-
return (
|
|
69
|
-
if (methods.includes(
|
|
70
|
-
return handler(
|
|
40
|
+
return (request) => {
|
|
41
|
+
if (methods.includes(request.method)) {
|
|
42
|
+
return handler(request);
|
|
71
43
|
}
|
|
72
|
-
|
|
73
44
|
throw new HttpError(405);
|
|
74
45
|
};
|
|
75
46
|
}
|
|
76
47
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
48
|
+
createServer(
|
|
49
|
+
createRequestListener(
|
|
50
|
+
allowedMethods(["GET"], greet)
|
|
51
|
+
)
|
|
52
|
+
).listen(8080);
|
|
80
53
|
```
|
|
81
54
|
|
|
82
|
-
The `allowedMethods` function is a factory that takes an array of allowed HTTP
|
|
83
|
-
methods and a handler. It creates a new handler that will invoke the given one
|
|
84
|
-
only if the method of the incoming request is included in the array of allowed
|
|
85
|
-
methods. Otherwise, a `Method Not Allowed` error is thrown.
|
|
86
|
-
|
|
87
55
|
## Error Handling
|
|
88
56
|
|
|
89
|
-
|
|
90
|
-
handler that catches the error that was thrown from the inner handler and
|
|
91
|
-
converts it to a result. As with any other handler, guards can be nested, giving
|
|
92
|
-
you fine grained control on how the error flows.
|
|
57
|
+
Lightpress supports flexible error handling at multiple levels. You can create special HTTP handlers that act as error guards. These guards allow you to control how specific parts of your handler tree respond to errors. For example, a guard around your rendering code could send errors as HTML, while a guard around your API could return JSON responses.
|
|
93
58
|
|
|
94
59
|
```js
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
function
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return {
|
|
106
|
-
headers: {
|
|
107
|
-
"Content-Type": "text/plain",
|
|
108
|
-
"Content-Length": body.length,
|
|
109
|
-
},
|
|
110
|
-
statusCode,
|
|
111
|
-
body,
|
|
112
|
-
};
|
|
113
|
-
});
|
|
60
|
+
import { HttpError } from "lightpress";
|
|
61
|
+
|
|
62
|
+
async function errorGuard(handler: HttpHandler) {
|
|
63
|
+
try {
|
|
64
|
+
return await handler(request);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
// Handle the error and return a result or re-throw the error
|
|
67
|
+
// to be handled by an upper guard.
|
|
68
|
+
}
|
|
114
69
|
}
|
|
115
|
-
|
|
116
|
-
// ...
|
|
117
|
-
|
|
118
|
-
const server = createServer(
|
|
119
|
-
lightpress(catchError(allowedMethods(["GET"], hello)))
|
|
120
|
-
);
|
|
121
70
|
```
|
|
122
71
|
|
|
123
|
-
|
|
124
|
-
response without content.
|
|
125
|
-
|
|
126
|
-
## Custom Data
|
|
127
|
-
|
|
128
|
-
The context object that is passed to a handler can be augmented with custom
|
|
129
|
-
data. Although it is technically possible to create a new copy of that context
|
|
130
|
-
object whenever you pass it on to the next handler, you most likely won't need
|
|
131
|
-
that. In fact, some 3rd-party packages might rely on using the same reference
|
|
132
|
-
and could break when creating a copy.
|
|
133
|
-
|
|
134
|
-
The recommended way to augement the context object, is by providing a handler
|
|
135
|
-
function that manipulates the context object. And another function that savely
|
|
136
|
-
returns the desired data from the context object. Or provides a fallback.
|
|
137
|
-
|
|
138
|
-
The following function adds a simple `log` function to the context object.
|
|
72
|
+
Additionally, any `HttpError` that reaches Lightpress’s root handler is considered a handled error and will be sent as an HTTP response. The `HttpError` constructor can receive either a status code or a full `HttpResult` object.
|
|
139
73
|
|
|
140
74
|
```js
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
75
|
+
// Only status code
|
|
76
|
+
throw new HttpError(404);
|
|
77
|
+
|
|
78
|
+
// With full HTTP result
|
|
79
|
+
throw new HttpError({
|
|
80
|
+
statusCode: 404,
|
|
81
|
+
headers: { "Content-Type": "text/plain" },
|
|
82
|
+
body: "Not found",
|
|
83
|
+
});
|
|
150
84
|
```
|
|
151
85
|
|
|
152
|
-
|
|
153
|
-
function.
|
|
86
|
+
Any other error is considered unexpected, and Lightpress will therefore respond with a generic `500` error. However, you can pass a `recover` function to `createRequestListener` as a second argument for global error handling.
|
|
154
87
|
|
|
155
88
|
```js
|
|
156
|
-
function
|
|
157
|
-
|
|
158
|
-
return context.log;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
console.warn("Trying to access logger, but was not injected.");
|
|
89
|
+
function recover(request, error) {
|
|
90
|
+
// Use this to run some cleanup code or do some logging.
|
|
162
91
|
|
|
163
|
-
return
|
|
92
|
+
return {
|
|
93
|
+
statusCode: 500,
|
|
94
|
+
headers: { "Content-Type": "text/plain" },
|
|
95
|
+
body: "Internal Server Error",
|
|
96
|
+
};
|
|
164
97
|
}
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
If no `log` function was injected into the context object, a warning is
|
|
168
|
-
printed and a `noop`-fallback is return instead.
|
|
169
98
|
|
|
170
|
-
|
|
171
|
-
|
|
99
|
+
createServer(
|
|
100
|
+
createRequestListener(greet, recover)
|
|
101
|
+
).listen(8080);
|
|
102
|
+
```
|
|
172
103
|
|
|
173
|
-
|
|
174
|
-
const log = extractLogger(context);
|
|
104
|
+
## Handler Factories
|
|
175
105
|
|
|
176
|
-
|
|
106
|
+
In real-world applications, it’s common to provide an HTTP handler by using a configurable factory. A factory can receive options, such as a database connection or other configuration, and returns an HTTP handler. This helps to decouple infrastructure from business logic and allows for simpler code reuse.
|
|
177
107
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
108
|
+
```js
|
|
109
|
+
function createApiHandler({ db }) {
|
|
110
|
+
return async (request) => {
|
|
111
|
+
const data = await db.getSomeData();
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
statusCode: 200,
|
|
115
|
+
headers: { "Content-Type": "application/json" },
|
|
116
|
+
body: JSON.stringify(data),
|
|
117
|
+
};
|
|
184
118
|
};
|
|
185
119
|
}
|
|
186
120
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
);
|
|
121
|
+
createServer(
|
|
122
|
+
createRequestListener(
|
|
123
|
+
createApiHandler({ db })
|
|
124
|
+
)
|
|
125
|
+
).listen(8080);
|
|
192
126
|
```
|
|
193
127
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
128
|
+
## Custom Handler Types and Context
|
|
129
|
+
|
|
130
|
+
Lightpress’s handler type is intentionally simple: it expects a function that receives a Node.js `IncomingMessage` and returns a result. Depending on your application, your HTTP handler may need additional request-related context, such as a timestamp, a user, or data that is expensive to retrieve. In this case, you will likely want to define your own handler type.
|
|
131
|
+
|
|
132
|
+
_TODO: add example_
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { IncomingMessage, OutgoingHttpHeaders, ServerResponse } from "node:http";
|
|
2
|
+
/** An object that is used to be send as HTTP response. */
|
|
3
|
+
export type HttpResult = null | undefined | {
|
|
4
|
+
/** Optional response status code (defaults to `200`). */
|
|
5
|
+
statusCode?: null | number;
|
|
6
|
+
/** Optional response body. */
|
|
7
|
+
body?: null | string | Buffer | NodeJS.ReadableStream;
|
|
8
|
+
/** Optional HTTP response headers. */
|
|
9
|
+
headers?: null | OutgoingHttpHeaders;
|
|
10
|
+
};
|
|
11
|
+
/** An HTTP request handler that creates an {@link HttpResult}. */
|
|
12
|
+
export type HttpHandler = (request: IncomingMessage) => HttpResult | Promise<HttpResult>;
|
|
13
|
+
/** A handler to recover from unhandled errors by the {@link HttpHandler}. */
|
|
14
|
+
export type RecoverHandler = (request: IncomingMessage, error: unknown) => HttpResult | Promise<HttpResult>;
|
|
15
|
+
/** Wraps an {@link HttpHandler} and returns a NodeJS request listener. */
|
|
16
|
+
export declare function createRequestListener(handler: HttpHandler, recover?: RecoverHandler): (request: IncomingMessage, response: ServerResponse) => Promise<void>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { sendResult } from "./send-result";
|
|
2
|
+
import { HttpError } from "./http-error";
|
|
3
|
+
/** Wraps an {@link HttpHandler} and returns a NodeJS request listener. */
|
|
4
|
+
export function createRequestListener(handler, recover) {
|
|
5
|
+
if (typeof handler !== "function") {
|
|
6
|
+
throw new TypeError("request handler must be a function");
|
|
7
|
+
}
|
|
8
|
+
return (request, response) => new Promise((resolve) => resolve(handler(request)))
|
|
9
|
+
.catch((error) => {
|
|
10
|
+
// Instances of `HttpError` are send as response, all other error types
|
|
11
|
+
// are considered unhandled.
|
|
12
|
+
if (error instanceof HttpError) {
|
|
13
|
+
return error;
|
|
14
|
+
}
|
|
15
|
+
if (recover) {
|
|
16
|
+
return recover(request, error);
|
|
17
|
+
}
|
|
18
|
+
throw error;
|
|
19
|
+
})
|
|
20
|
+
.catch((error) => {
|
|
21
|
+
// Log unhandled error if no recover handler is defined or an error was
|
|
22
|
+
// thrown from the recover handler itself.
|
|
23
|
+
console.error(error);
|
|
24
|
+
return { statusCode: 500 };
|
|
25
|
+
})
|
|
26
|
+
.then((result) => sendResult(response, result));
|
|
27
|
+
}
|
package/lib/http-error.d.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
/**
|
|
4
|
-
export declare class HttpError extends Error implements
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
constructor(statusCode: number);
|
|
12
|
-
/** Converts the error to an HTTP result object. */
|
|
13
|
-
toResult(): LightpressResult;
|
|
1
|
+
import type { OutgoingHttpHeaders } from "node:http";
|
|
2
|
+
import type { HttpResult } from "./create-request-listener";
|
|
3
|
+
/** An error that can be send as an HTTP result. */
|
|
4
|
+
export declare class HttpError extends Error implements NonNullable<HttpResult> {
|
|
5
|
+
name: string;
|
|
6
|
+
statusCode: number;
|
|
7
|
+
body?: null | string | Buffer | NodeJS.ReadableStream;
|
|
8
|
+
headers?: null | OutgoingHttpHeaders;
|
|
9
|
+
constructor(result: HttpResult, options?: ErrorOptions);
|
|
10
|
+
constructor(statusCode: number, options?: ErrorOptions);
|
|
14
11
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { STATUS_CODES } from "node:http";
|
|
2
|
+
/** An error that can be send as an HTTP result. */
|
|
3
|
+
export class HttpError extends Error {
|
|
4
|
+
name = "HttpError";
|
|
5
|
+
statusCode;
|
|
6
|
+
body;
|
|
7
|
+
headers;
|
|
8
|
+
constructor(resultOrStatusCode, options) {
|
|
9
|
+
const [statusCode, result] = typeof resultOrStatusCode === "number"
|
|
10
|
+
? [resultOrStatusCode, null]
|
|
11
|
+
: [resultOrStatusCode?.statusCode ?? 500, resultOrStatusCode];
|
|
12
|
+
super(STATUS_CODES[statusCode], options);
|
|
13
|
+
// TODO: investigate if this is really needed
|
|
14
|
+
if (Error.captureStackTrace) {
|
|
15
|
+
Error.captureStackTrace(this, this.constructor);
|
|
16
|
+
}
|
|
17
|
+
this.statusCode = statusCode;
|
|
18
|
+
this.body = result?.body;
|
|
19
|
+
this.headers = result?.headers;
|
|
20
|
+
}
|
|
21
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
export * from "./
|
|
2
|
-
export * from "./types/lightpress-error";
|
|
3
|
-
export * from "./types/lightpress-handler";
|
|
4
|
-
export * from "./types/lightpress-recovery-handler";
|
|
5
|
-
export * from "./types/lightpress-result";
|
|
6
|
-
export * from "./from-lightpress-error";
|
|
1
|
+
export * from "./create-request-listener";
|
|
7
2
|
export * from "./http-error";
|
|
8
|
-
export
|
|
9
|
-
export * from "./lightpress";
|
|
10
|
-
export { lightpress as default } from "./lightpress";
|
|
3
|
+
export { createRequestListener as default } from "./create-request-listener";
|
package/lib/index.js
CHANGED
|
@@ -1,143 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
var http = require('http');
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Typeguard to test if the given object implements the `LightpressError`
|
|
9
|
-
* interface.
|
|
10
|
-
*/
|
|
11
|
-
function isLightpressError(error) {
|
|
12
|
-
return Boolean(error && typeof error.toResult === "function");
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Wraps a recover function to automatically recover from `LightpressError`s by
|
|
17
|
-
* calling `toResult()` on it. All other errors are passed on to the given
|
|
18
|
-
* recover function.
|
|
19
|
-
* **WARNING:** does not catch errors from the recover function itself!
|
|
20
|
-
*/
|
|
21
|
-
function fromLightpressError(
|
|
22
|
-
recoverUnhandled
|
|
23
|
-
) {
|
|
24
|
-
return (request, error) => {
|
|
25
|
-
if (isLightpressError(error)) {
|
|
26
|
-
try {
|
|
27
|
-
return error.toResult();
|
|
28
|
-
} catch (exception) {
|
|
29
|
-
return recoverUnhandled(request, exception);
|
|
30
|
-
}
|
|
31
|
-
} else {
|
|
32
|
-
return recoverUnhandled(request, error);
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Basic error implementation to signal HTTP errors. */
|
|
38
|
-
class HttpError extends Error {
|
|
39
|
-
__init() {this.name = "HttpError";}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* The HTTP error represents an error based on the HTTP error codes.
|
|
44
|
-
* @param statusCode An HTTP status code
|
|
45
|
-
*/
|
|
46
|
-
constructor(statusCode) {
|
|
47
|
-
super(http.STATUS_CODES[statusCode]);HttpError.prototype.__init.call(this);
|
|
48
|
-
if (Error.captureStackTrace) {
|
|
49
|
-
Error.captureStackTrace(this, this.constructor);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
this.statusCode = statusCode;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Converts the error to an HTTP result object. */
|
|
56
|
-
toResult() {
|
|
57
|
-
return { statusCode: this.statusCode };
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Typeguard to test if the given object is a readable stream. */
|
|
62
|
-
function isReadableStream(data) {
|
|
63
|
-
return Boolean(
|
|
64
|
-
data &&
|
|
65
|
-
data.readable === true &&
|
|
66
|
-
typeof data.pipe === "function" &&
|
|
67
|
-
typeof data._read === "function"
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Takes a `LightpressResult` and sends it as HTTP response. */
|
|
72
|
-
function sendResult(
|
|
73
|
-
response,
|
|
74
|
-
result
|
|
75
|
-
) {
|
|
76
|
-
const statusCode = result && result.statusCode ? result.statusCode : 200;
|
|
77
|
-
const headers = result && result.headers ? result.headers : null;
|
|
78
|
-
const body = result && result.body ? result.body : null;
|
|
79
|
-
|
|
80
|
-
if (headers) {
|
|
81
|
-
response.writeHead(statusCode, headers);
|
|
82
|
-
} else {
|
|
83
|
-
response.statusCode = statusCode;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (isReadableStream(body)) {
|
|
87
|
-
body.pipe(response);
|
|
88
|
-
} else {
|
|
89
|
-
response.end(body);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Wraps a `LightpressHandler` into a function that directly be bound as handler
|
|
95
|
-
* for an HTTP server's incoming `request` events.
|
|
96
|
-
*/
|
|
97
|
-
function lightpress(
|
|
98
|
-
handler,
|
|
99
|
-
recover
|
|
100
|
-
) {
|
|
101
|
-
if (typeof handler !== "function") {
|
|
102
|
-
throw new TypeError("request handler must be a function");
|
|
103
|
-
}
|
|
104
|
-
if (recover && typeof recover !== "function") {
|
|
105
|
-
throw new TypeError("recovery handler must be a function");
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Although it requires a little bit more boilerplate code, it is expected
|
|
109
|
-
// that the given recover handler cares about `LightpressError`s itself. In
|
|
110
|
-
// total, this provides more control over the error recovery.
|
|
111
|
-
const innerRecover =
|
|
112
|
-
recover || fromLightpressError(() => ({ statusCode: 500 }));
|
|
113
|
-
|
|
114
|
-
return (request, response) =>
|
|
115
|
-
// Directly return the promise so that it's resolution can be tracked
|
|
116
|
-
// outside, e.g. in unit tests.
|
|
117
|
-
new Promise((resolve) => resolve(handler({ request })))
|
|
118
|
-
// Instead of the context object, the request is passed to the recovery
|
|
119
|
-
// handler as we cannot know if the reference to the context has changed
|
|
120
|
-
// during handler invokation. Assumably, it would lead to more confussion
|
|
121
|
-
// about possibly missing properties than it would help.
|
|
122
|
-
.catch((error) => innerRecover(request, error))
|
|
123
|
-
.then((result) => sendResult(response, result))
|
|
124
|
-
// Fallback guard to prevent the application to crash if error recovery
|
|
125
|
-
// or sending the response have failed.
|
|
126
|
-
.catch((error) => {
|
|
127
|
-
console.error(error);
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
// TODO: investigate if closing the connection is a better option
|
|
131
|
-
request.pause();
|
|
132
|
-
response.end();
|
|
133
|
-
} catch (exception) {
|
|
134
|
-
console.error(exception);
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
exports.HttpError = HttpError;
|
|
140
|
-
exports.default = lightpress;
|
|
141
|
-
exports.fromLightpressError = fromLightpressError;
|
|
142
|
-
exports.isLightpressError = isLightpressError;
|
|
143
|
-
exports.lightpress = lightpress;
|
|
1
|
+
export * from "./create-request-listener";
|
|
2
|
+
export * from "./http-error";
|
|
3
|
+
export { createRequestListener as default } from "./create-request-listener";
|
package/lib/send-result.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export declare function sendResult(response: ServerResponse, result: LightpressResult): void;
|
|
1
|
+
import type { ServerResponse } from "node:http";
|
|
2
|
+
import type { HttpResult } from "./create-request-listener";
|
|
3
|
+
/** Passes the given {@link HttpResult} to the given {@link ServerResponse}. */
|
|
4
|
+
export declare function sendResult(response: ServerResponse, result: HttpResult): void;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { isReadableStream } from "./is-readable-stream";
|
|
2
|
+
/** Passes the given {@link HttpResult} to the given {@link ServerResponse}. */
|
|
3
|
+
export function sendResult(response, result) {
|
|
4
|
+
const statusCode = result?.statusCode ?? 200;
|
|
5
|
+
if (result?.headers) {
|
|
6
|
+
response.writeHead(statusCode, result.headers);
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
response.statusCode = statusCode;
|
|
10
|
+
}
|
|
11
|
+
if (isReadableStream(result?.body)) {
|
|
12
|
+
result.body.pipe(response);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
response.end(result?.body ?? null);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { IncomingHttpHeaders, IncomingMessage } from "node:http";
|
|
2
|
+
export declare function assertContentType<const TContentType extends string>(request: IncomingMessage, contentType: TContentType): asserts request is IncomingMessage & {
|
|
3
|
+
headers: IncomingHttpHeaders & {
|
|
4
|
+
["content-type"]: TContentType;
|
|
5
|
+
};
|
|
6
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { HttpError } from "../http-error";
|
|
2
|
+
export async function consumeBody(request, maxByteLength) {
|
|
3
|
+
const chunks = [];
|
|
4
|
+
let chunksBytes = 0;
|
|
5
|
+
for await (const chunk of request.iterator()) {
|
|
6
|
+
chunks.push(chunk);
|
|
7
|
+
chunksBytes = chunksBytes + chunk.byteLength;
|
|
8
|
+
if (maxByteLength && chunksBytes > maxByteLength) {
|
|
9
|
+
chunks.length = 0;
|
|
10
|
+
chunksBytes = 0;
|
|
11
|
+
throw new HttpError(413);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return Buffer.concat(chunks, chunksBytes);
|
|
15
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { HttpError } from "../http-error";
|
|
2
|
+
import { consumeBody } from "./consume-body";
|
|
3
|
+
export async function consumeJsonBody(request, maxByteLength) {
|
|
4
|
+
const buffer = await consumeBody(request, maxByteLength);
|
|
5
|
+
try {
|
|
6
|
+
return JSON.parse(buffer.toString("utf8"));
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
throw new HttpError(400, {
|
|
10
|
+
cause: error,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,54 +1,54 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
2
|
+
"name": "lightpress",
|
|
3
|
+
"version": "2.0.0-beta.1",
|
|
4
|
+
"description": "A thin composable HTTP handler interface around NodeJS native server listener.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"typings": "lib/index.d.ts",
|
|
7
|
+
"main": "lib/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./lib/index.js",
|
|
10
|
+
"./utility": "./lib/utility/index.js",
|
|
11
|
+
"./package.json": "./package.json"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=22.0.0"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "rimraf ./lib && tsc",
|
|
18
|
+
"check-format": "biome check .",
|
|
19
|
+
"check-types": "tsc --noEmit",
|
|
20
|
+
"format": "biome check --write .",
|
|
21
|
+
"prepublishOnly": "npm run check-format && npm run test && npm run build",
|
|
22
|
+
"test": "jest"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"lib/**/*"
|
|
26
|
+
],
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/lunsdorf/lightpress.git"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"composable",
|
|
33
|
+
"express",
|
|
34
|
+
"http",
|
|
35
|
+
"lightpress",
|
|
36
|
+
"server"
|
|
37
|
+
],
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"author": "Lunsdorf <lunsdorf@users.noreply.github.com>",
|
|
40
|
+
"homepage": "https://github.com/lunsdorf/lightpress#readme",
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/lunsdorf/lightpress/issues"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@biomejs/biome": "^2.3.11",
|
|
46
|
+
"@swc/core": "^1.15.8",
|
|
47
|
+
"@swc/jest": "^0.2.39",
|
|
48
|
+
"@types/jest": "^30.0.0",
|
|
49
|
+
"@types/node": "^22.0.0",
|
|
50
|
+
"jest": "^30.2.0",
|
|
51
|
+
"rimraf": "^6.1.2",
|
|
52
|
+
"typescript": "^5.9.3"
|
|
53
|
+
}
|
|
54
54
|
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { LightpressRecoveryHandler } from "./types/lightpress-recovery-handler";
|
|
2
|
-
/**
|
|
3
|
-
* Wraps a recover function to automatically recover from `LightpressError`s by
|
|
4
|
-
* calling `toResult()` on it. All other errors are passed on to the given
|
|
5
|
-
* recover function.
|
|
6
|
-
* **WARNING:** does not catch errors from the recover function itself!
|
|
7
|
-
*/
|
|
8
|
-
export declare function fromLightpressError(recoverUnhandled: LightpressRecoveryHandler): LightpressRecoveryHandler;
|
package/lib/index.mjs
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { STATUS_CODES } from 'http';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Typeguard to test if the given object implements the `LightpressError`
|
|
5
|
-
* interface.
|
|
6
|
-
*/
|
|
7
|
-
function isLightpressError(error) {
|
|
8
|
-
return Boolean(error && typeof error.toResult === "function");
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Wraps a recover function to automatically recover from `LightpressError`s by
|
|
13
|
-
* calling `toResult()` on it. All other errors are passed on to the given
|
|
14
|
-
* recover function.
|
|
15
|
-
* **WARNING:** does not catch errors from the recover function itself!
|
|
16
|
-
*/
|
|
17
|
-
function fromLightpressError(
|
|
18
|
-
recoverUnhandled
|
|
19
|
-
) {
|
|
20
|
-
return (request, error) => {
|
|
21
|
-
if (isLightpressError(error)) {
|
|
22
|
-
try {
|
|
23
|
-
return error.toResult();
|
|
24
|
-
} catch (exception) {
|
|
25
|
-
return recoverUnhandled(request, exception);
|
|
26
|
-
}
|
|
27
|
-
} else {
|
|
28
|
-
return recoverUnhandled(request, error);
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Basic error implementation to signal HTTP errors. */
|
|
34
|
-
class HttpError extends Error {
|
|
35
|
-
__init() {this.name = "HttpError";}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* The HTTP error represents an error based on the HTTP error codes.
|
|
40
|
-
* @param statusCode An HTTP status code
|
|
41
|
-
*/
|
|
42
|
-
constructor(statusCode) {
|
|
43
|
-
super(STATUS_CODES[statusCode]);HttpError.prototype.__init.call(this);
|
|
44
|
-
if (Error.captureStackTrace) {
|
|
45
|
-
Error.captureStackTrace(this, this.constructor);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
this.statusCode = statusCode;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Converts the error to an HTTP result object. */
|
|
52
|
-
toResult() {
|
|
53
|
-
return { statusCode: this.statusCode };
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Typeguard to test if the given object is a readable stream. */
|
|
58
|
-
function isReadableStream(data) {
|
|
59
|
-
return Boolean(
|
|
60
|
-
data &&
|
|
61
|
-
data.readable === true &&
|
|
62
|
-
typeof data.pipe === "function" &&
|
|
63
|
-
typeof data._read === "function"
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** Takes a `LightpressResult` and sends it as HTTP response. */
|
|
68
|
-
function sendResult(
|
|
69
|
-
response,
|
|
70
|
-
result
|
|
71
|
-
) {
|
|
72
|
-
const statusCode = result && result.statusCode ? result.statusCode : 200;
|
|
73
|
-
const headers = result && result.headers ? result.headers : null;
|
|
74
|
-
const body = result && result.body ? result.body : null;
|
|
75
|
-
|
|
76
|
-
if (headers) {
|
|
77
|
-
response.writeHead(statusCode, headers);
|
|
78
|
-
} else {
|
|
79
|
-
response.statusCode = statusCode;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (isReadableStream(body)) {
|
|
83
|
-
body.pipe(response);
|
|
84
|
-
} else {
|
|
85
|
-
response.end(body);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Wraps a `LightpressHandler` into a function that directly be bound as handler
|
|
91
|
-
* for an HTTP server's incoming `request` events.
|
|
92
|
-
*/
|
|
93
|
-
function lightpress(
|
|
94
|
-
handler,
|
|
95
|
-
recover
|
|
96
|
-
) {
|
|
97
|
-
if (typeof handler !== "function") {
|
|
98
|
-
throw new TypeError("request handler must be a function");
|
|
99
|
-
}
|
|
100
|
-
if (recover && typeof recover !== "function") {
|
|
101
|
-
throw new TypeError("recovery handler must be a function");
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Although it requires a little bit more boilerplate code, it is expected
|
|
105
|
-
// that the given recover handler cares about `LightpressError`s itself. In
|
|
106
|
-
// total, this provides more control over the error recovery.
|
|
107
|
-
const innerRecover =
|
|
108
|
-
recover || fromLightpressError(() => ({ statusCode: 500 }));
|
|
109
|
-
|
|
110
|
-
return (request, response) =>
|
|
111
|
-
// Directly return the promise so that it's resolution can be tracked
|
|
112
|
-
// outside, e.g. in unit tests.
|
|
113
|
-
new Promise((resolve) => resolve(handler({ request })))
|
|
114
|
-
// Instead of the context object, the request is passed to the recovery
|
|
115
|
-
// handler as we cannot know if the reference to the context has changed
|
|
116
|
-
// during handler invokation. Assumably, it would lead to more confussion
|
|
117
|
-
// about possibly missing properties than it would help.
|
|
118
|
-
.catch((error) => innerRecover(request, error))
|
|
119
|
-
.then((result) => sendResult(response, result))
|
|
120
|
-
// Fallback guard to prevent the application to crash if error recovery
|
|
121
|
-
// or sending the response have failed.
|
|
122
|
-
.catch((error) => {
|
|
123
|
-
console.error(error);
|
|
124
|
-
|
|
125
|
-
try {
|
|
126
|
-
// TODO: investigate if closing the connection is a better option
|
|
127
|
-
request.pause();
|
|
128
|
-
response.end();
|
|
129
|
-
} catch (exception) {
|
|
130
|
-
console.error(exception);
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export default lightpress;
|
|
136
|
-
export { HttpError, fromLightpressError, isLightpressError, lightpress };
|
package/lib/lightpress.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
import { IncomingMessage, ServerResponse } from "http";
|
|
3
|
-
import { LightpressContext } from "./types/lightpress-context";
|
|
4
|
-
import { LightpressHandler } from "./types/lightpress-handler";
|
|
5
|
-
import { LightpressRecoveryHandler } from "./types/lightpress-recovery-handler";
|
|
6
|
-
/**
|
|
7
|
-
* Wraps a `LightpressHandler` into a function that directly be bound as handler
|
|
8
|
-
* for an HTTP server's incoming `request` events.
|
|
9
|
-
*/
|
|
10
|
-
export declare function lightpress(handler: LightpressHandler<LightpressContext>, recover?: LightpressRecoveryHandler): (request: IncomingMessage, response: ServerResponse) => Promise<void>;
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { LightpressResult } from "./lightpress-result";
|
|
2
|
-
/**
|
|
3
|
-
* Represents a handled error result that fits the dataflow of promises and can
|
|
4
|
-
* be recoverd from.
|
|
5
|
-
* NOTE: Intentionally, this is not designed as a common result factory as it
|
|
6
|
-
* would make dataflow overly complicated and can already be archieved through
|
|
7
|
-
* the use of promises itself.
|
|
8
|
-
*/
|
|
9
|
-
export interface LightpressError extends Error {
|
|
10
|
-
toResult(): LightpressResult;
|
|
11
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { LightpressContext } from "./lightpress-context";
|
|
2
|
-
import { LightpressResult } from "./lightpress-result";
|
|
3
|
-
/**
|
|
4
|
-
* Describes a function that creates a `LightpressResult` for incoming HTTP
|
|
5
|
-
* requests.
|
|
6
|
-
*/
|
|
7
|
-
export declare type LightpressHandler<T extends LightpressContext> = (context: T) => LightpressResult | Promise<LightpressResult>;
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
import { IncomingMessage } from "http";
|
|
3
|
-
import { LightpressResult } from "./lightpress-result";
|
|
4
|
-
/** Describes a function to convert an error to a `LightpressResult`. */
|
|
5
|
-
export declare type LightpressRecoveryHandler = (request: IncomingMessage, error: Error) => LightpressResult | Promise<LightpressResult>;
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
import { OutgoingHttpHeaders } from "http";
|
|
3
|
-
/** An object that is used to be send as HTTP response. */
|
|
4
|
-
export declare type LightpressResult = void | null | {
|
|
5
|
-
/** Optional response status code (defaults to `200`). */
|
|
6
|
-
statusCode?: null | number;
|
|
7
|
-
/** Optional response payload. */
|
|
8
|
-
body?: null | string | Buffer | NodeJS.ReadableStream;
|
|
9
|
-
/** Optional HTTP response headers. */
|
|
10
|
-
headers?: null | OutgoingHttpHeaders;
|
|
11
|
-
};
|