fulmine.js 5.12.2 → 5.13.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 +83 -29
- package/package.json +33 -3
- package/src/adopt.js +324 -0
- package/src/application.js +2 -2
- package/src/cli.js +25 -6
- package/src/declarative.js +17 -9
- package/src/middlewares.js +11 -4
- package/src/nest.d.ts +39 -0
- package/src/nest.js +119 -0
- package/src/request.js +103 -22
- package/src/response.js +24 -3
- package/src/router.js +72 -13
- package/src/testing.js +27 -3
- package/src/utils.js +22 -2
package/src/cli.js
CHANGED
|
@@ -31,6 +31,13 @@ limitations under the License.
|
|
|
31
31
|
//
|
|
32
32
|
// Whether this machine and this project can run it at all: the node version, the C library, the
|
|
33
33
|
// µWebSockets.js binary, the base image a Dockerfile names. See src/verify.js.
|
|
34
|
+
//
|
|
35
|
+
// npx fulmine override [dir]
|
|
36
|
+
// npx fulmine angular [dir]
|
|
37
|
+
//
|
|
38
|
+
// The two things a project needs that are a line in a JSON file rather than a specifier in a source
|
|
39
|
+
// file: the package manager substitution, for a framework that requires express in its own code,
|
|
40
|
+
// and angular.json's externalDependencies. See src/adopt.js.
|
|
34
41
|
|
|
35
42
|
const fs = require("fs");
|
|
36
43
|
const path = require("path");
|
|
@@ -38,6 +45,7 @@ const acorn = require("acorn");
|
|
|
38
45
|
// the same walk express.testing asserts on, so the command and the assertions cannot drift
|
|
39
46
|
const { collectRoutes } = require("./testing.js");
|
|
40
47
|
const { verify } = require("./verify.js");
|
|
48
|
+
const { override, angular } = require("./adopt.js");
|
|
41
49
|
|
|
42
50
|
const FROM = "express";
|
|
43
51
|
const TO = "fulmine.js";
|
|
@@ -85,11 +93,12 @@ const DIFFERENCES = [
|
|
|
85
93
|
'Express sends X-Powered-By: Express unless told not to. Set app.set("x-powered-by", true) to send it.'
|
|
86
94
|
],
|
|
87
95
|
[
|
|
88
|
-
"a compiled route
|
|
89
|
-
"A handler simple enough to be read at registration time is answered natively
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
|
|
96
|
+
"a compiled route keeps its connection header",
|
|
97
|
+
"A handler simple enough to be read at registration time is answered natively, and a client\n" +
|
|
98
|
+
"that sent Connection: close is still told keep-alive, though the socket does close. A body\n" +
|
|
99
|
+
"with a piece of the query in it is framed chunked, since its length is not known until the\n" +
|
|
100
|
+
"request arrives. A response that would carry a validator is never compiled, so conditional\n" +
|
|
101
|
+
'requests behave as on Express. app.set("declarative responses", false) turns it off.'
|
|
93
102
|
],
|
|
94
103
|
[
|
|
95
104
|
"headers are capped at 4096 bytes by default",
|
|
@@ -822,9 +831,19 @@ function main(argv) {
|
|
|
822
831
|
if (command === "explain") {
|
|
823
832
|
return explain(argv.slice(1));
|
|
824
833
|
}
|
|
834
|
+
if (command === "override") {
|
|
835
|
+
return override(argv.slice(1));
|
|
836
|
+
}
|
|
837
|
+
if (command === "angular") {
|
|
838
|
+
return angular(argv.slice(1));
|
|
839
|
+
}
|
|
825
840
|
if (command !== "migrate") {
|
|
826
841
|
console.log(`Usage:
|
|
827
842
|
npx ${TO} migrate [dir] rewrite require("${FROM}") and import from "${FROM}" to "${TO}"
|
|
843
|
+
npx ${TO} override [dir] answer ${FROM} with this package for the whole dependency tree, for
|
|
844
|
+
when a framework requires ${FROM} in its own code and not in yours
|
|
845
|
+
npx ${TO} angular [dir] declare this package external in angular.json's server build, which
|
|
846
|
+
esbuild otherwise tries to inline a native binary into
|
|
828
847
|
npx ${TO} profile [entry] load an application without listening and print what compiling
|
|
829
848
|
its routes decided, route by route
|
|
830
849
|
npx ${TO} explain <route> what happens when a request for that route arrives
|
|
@@ -832,7 +851,7 @@ function main(argv) {
|
|
|
832
851
|
npx ${TO} differences print what behaves differently, without changing anything
|
|
833
852
|
|
|
834
853
|
Options:
|
|
835
|
-
--dry-run migrate: say what would change and change nothing`);
|
|
854
|
+
--dry-run migrate, override, angular: say what would change and change nothing`);
|
|
836
855
|
return command ? 1 : 0;
|
|
837
856
|
}
|
|
838
857
|
|
package/src/declarative.js
CHANGED
|
@@ -54,6 +54,11 @@ const MAX_INSTRUCTION_LENGTH = 65535;
|
|
|
54
54
|
// so it cannot honour one, and a handler that sets one has to stay on the ordinary path.
|
|
55
55
|
const VALIDATOR_HEADERS = new Set(["etag", "last-modified"]);
|
|
56
56
|
|
|
57
|
+
// The statuses whose message carries no content. 205 is here for node's reason rather than
|
|
58
|
+
// express's: express strips the body for 204 and 304, and node answers a 205 with a lone
|
|
59
|
+
// Content-Length of zero, so all three come out of the ordinary path with no body at all.
|
|
60
|
+
const BODILESS_STATUSES = new Set([204, 205, 304]);
|
|
61
|
+
|
|
57
62
|
const bodyMethods = new Set(["send", "json", "end"]);
|
|
58
63
|
// and the four that finish the response, after which nothing a handler does is observable
|
|
59
64
|
const terminalMethods = new Set(["send", "json", "end", "sendStatus"]);
|
|
@@ -710,6 +715,14 @@ module.exports = function compileDeclarative(cb, app) {
|
|
|
710
715
|
return false;
|
|
711
716
|
}
|
|
712
717
|
|
|
718
|
+
// A status that carries no content. Compiled, the body went out with it, and a client
|
|
719
|
+
// frames these as bodiless whatever the headers say, so those bytes were read as the start
|
|
720
|
+
// of the next answer on the connection. The ordinary path already writes all three the way
|
|
721
|
+
// express does.
|
|
722
|
+
if (BODILESS_STATUSES.has(statusCode) || statusCode < 200) {
|
|
723
|
+
return false;
|
|
724
|
+
}
|
|
725
|
+
|
|
713
726
|
let decRes = new uWSAny.DeclarativeResponse();
|
|
714
727
|
|
|
715
728
|
if (statusCode !== 200) {
|
|
@@ -755,15 +768,10 @@ module.exports = function compileDeclarative(cb, app) {
|
|
|
755
768
|
body.push({ type: "text", value: statuses.message[statusCode] || String(statusCode) });
|
|
756
769
|
}
|
|
757
770
|
|
|
758
|
-
// A response that would carry a validator is not compiled at all
|
|
759
|
-
//
|
|
760
|
-
//
|
|
761
|
-
//
|
|
762
|
-
// then ignore it, so every revalidation got 200 and the whole body where Express answers
|
|
763
|
-
// 304 with none. Dropping the ETag instead would have kept the route compiled, at the
|
|
764
|
-
// price of no validator at all on the simplest routes of every application. Refusing
|
|
765
|
-
// keeps Express's answer, and `etag` false is how a route that does not need one stays
|
|
766
|
-
// compiled, which is what both benchmarks here already set.
|
|
771
|
+
// A response that would carry a validator is not compiled at all: µWS answers it without
|
|
772
|
+
// reading the request, so it could never turn a conditional GET into the 304 the validator
|
|
773
|
+
// invites. Dropping the ETag instead would leave the simplest routes of an application
|
|
774
|
+
// without one, so `etag` false is how a route stays compiled.
|
|
767
775
|
if (headers.some((header) => VALIDATOR_HEADERS.has(header[0].toLowerCase()))) {
|
|
768
776
|
return false;
|
|
769
777
|
}
|
package/src/middlewares.js
CHANGED
|
@@ -810,13 +810,20 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
810
810
|
return next();
|
|
811
811
|
}
|
|
812
812
|
|
|
813
|
+
// The property goes on the request before anything is decided, and its value stays
|
|
814
|
+
// undefined: body-parser's read() does exactly this, and the two halves both matter.
|
|
815
|
+
// Undefined, so a handler can still tell "nothing parsed this" from "the body was
|
|
816
|
+
// empty", which seeding an empty object would lose. Present, because `"body" in req`
|
|
817
|
+
// is how a library asks whether a parser has run at all: Apollo's express middleware
|
|
818
|
+
// refuses the request with a 500 when the property is missing, and tRPC's adapter
|
|
819
|
+
// reads the body itself when it is, so getting either half wrong breaks one of them.
|
|
820
|
+
if (!("body" in req)) {
|
|
821
|
+
req.body = undefined;
|
|
822
|
+
}
|
|
823
|
+
|
|
813
824
|
// straight from the raw entries: three headers do not justify building the object
|
|
814
825
|
const type = req._rawHeader("content-type");
|
|
815
826
|
|
|
816
|
-
// req.body is deliberately left undefined until a parser claims the request. That is
|
|
817
|
-
// what lets a handler tell "nothing parsed this" apart from "the body was empty",
|
|
818
|
-
// so it must not be seeded with an empty object first.
|
|
819
|
-
|
|
820
827
|
// skip reading body for no content type
|
|
821
828
|
// a function decides for itself, and body-parser lets it see a request that carries no
|
|
822
829
|
// content-type at all. Only the string and array forms need one to match against
|
package/src/nest.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2026 Nigro Simone
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { ExpressAdapter } from "@nestjs/platform-express";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Nest's Express adapter, listening on uWebSockets.js instead of on node.
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { NestFactory } from "@nestjs/core";
|
|
24
|
+
* import { FulmineExpressAdapter } from "fulmine.js/nest";
|
|
25
|
+
*
|
|
26
|
+
* const app = await NestFactory.create(AppModule, new FulmineExpressAdapter());
|
|
27
|
+
* await app.listen(3000);
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* `@nestjs/platform-express` is an optional peer dependency: this entry point is the only thing
|
|
31
|
+
* that needs it, and nothing loads it unless you import this.
|
|
32
|
+
*/
|
|
33
|
+
export declare class FulmineExpressAdapter extends ExpressAdapter {
|
|
34
|
+
/**
|
|
35
|
+
* @param instance an application from `fulmine()`; one is created when omitted. Pass your own
|
|
36
|
+
* when it needs options, TLS being the usual reason: `fulmine({ uwsOptions })`.
|
|
37
|
+
*/
|
|
38
|
+
constructor(instance?: any);
|
|
39
|
+
}
|
package/src/nest.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2026 Nigro Simone
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// require("fulmine.js/nest"): the Nest HTTP adapter, so a Nest application runs on µWS without
|
|
18
|
+
// anyone having to write this file themselves.
|
|
19
|
+
//
|
|
20
|
+
// import { NestFactory } from "@nestjs/core";
|
|
21
|
+
// import { FulmineExpressAdapter } from "fulmine.js/nest";
|
|
22
|
+
//
|
|
23
|
+
// const app = await NestFactory.create(AppModule, new FulmineExpressAdapter());
|
|
24
|
+
// await app.listen(3000);
|
|
25
|
+
//
|
|
26
|
+
// @nestjs/platform-express takes any Express instance, and this is one, so everything above the
|
|
27
|
+
// adapter - controllers, pipes, guards, interceptors - is untouched. Three things below it are not,
|
|
28
|
+
// and they are the whole reason this file exists:
|
|
29
|
+
//
|
|
30
|
+
// - initHttpServer wraps the instance in http.createServer() and listens on that. Every request
|
|
31
|
+
// would then arrive through node's parser and be replayed into µWS's shapes by node-shim.js,
|
|
32
|
+
// which is the slow path that exists for supertest. The app already answers as an http.Server,
|
|
33
|
+
// so it is the server instead of being put inside one.
|
|
34
|
+
// - registerParserMiddleware decides whether Nest's body parsers are already in the chain by
|
|
35
|
+
// scanning app.router.stack for them. There is no layer array here to scan, routes are compiled
|
|
36
|
+
// rather than kept as layers, so the answer was always "no" and a second call added a second
|
|
37
|
+
// pair. It is remembered here instead, which is the same answer by a different route.
|
|
38
|
+
// - httpsOptions asks node to make a TLS server out of the instance. TLS here belongs to µWS and
|
|
39
|
+
// is configured when the app is built, so that combination is refused with the line to write
|
|
40
|
+
// rather than silently starting a plaintext server.
|
|
41
|
+
//
|
|
42
|
+
// @nestjs/platform-express is an optional peer dependency: this file is the only one that requires
|
|
43
|
+
// it, and nothing loads this file unless you ask for it by name.
|
|
44
|
+
|
|
45
|
+
"use strict";
|
|
46
|
+
|
|
47
|
+
const { ExpressAdapter } = require("@nestjs/platform-express");
|
|
48
|
+
const fulmine = require("./index.js");
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Nest's Express adapter, listening on µWebSockets.js instead of on node.
|
|
52
|
+
*
|
|
53
|
+
* Pass a configured app when you need one, `new FulmineExpressAdapter(fulmine({ uwsOptions }))`;
|
|
54
|
+
* with no argument it builds a default one, the same as `new ExpressAdapter()` does.
|
|
55
|
+
*/
|
|
56
|
+
class FulmineExpressAdapter extends ExpressAdapter {
|
|
57
|
+
/**
|
|
58
|
+
* @param {any} [instance] an application from `fulmine()`; one is created when omitted
|
|
59
|
+
*/
|
|
60
|
+
constructor(instance) {
|
|
61
|
+
super(instance || fulmine());
|
|
62
|
+
/**
|
|
63
|
+
* Whether Nest's body parsers are in the chain, standing in for the layer array Express
|
|
64
|
+
* has and this does not. See registerParserMiddleware below.
|
|
65
|
+
* @type {boolean}
|
|
66
|
+
*/
|
|
67
|
+
this._parsersRegistered = false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The app is the server. Nest calls this once, from NestApplication's constructor.
|
|
72
|
+
*
|
|
73
|
+
* @param {any} [options] the options NestFactory.create was given
|
|
74
|
+
* @returns {void}
|
|
75
|
+
*/
|
|
76
|
+
initHttpServer(options) {
|
|
77
|
+
if (options?.httpsOptions) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
"fulmine.js: httpsOptions cannot be used here, since there is no node server to give " +
|
|
80
|
+
"them to. TLS belongs to µWS and is configured when the app is built:\n" +
|
|
81
|
+
' new FulmineExpressAdapter(fulmine({ uwsOptions: { key_file_name: "key.pem", cert_file_name: "cert.pem" } }))'
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
this.httpServer = this.getInstance();
|
|
85
|
+
if (options?.forceCloseConnections) {
|
|
86
|
+
// trackOpenConnections() listens for 'connection', which nothing emits: the sockets
|
|
87
|
+
// belong to µWS and never become node ones. Said out loud, because a shutdown that
|
|
88
|
+
// quietly waits forever for what it thinks it can destroy is worse than one that does
|
|
89
|
+
// not offer to. Through Nest's own logger, so the line arrives where every other line
|
|
90
|
+
// from the framework does; it is private in the typings and inherited all the same
|
|
91
|
+
/** @type {any} */ (this).logger.warn(
|
|
92
|
+
"forceCloseConnections has no effect on fulmine.js: the sockets belong to µWS. " +
|
|
93
|
+
"app.close() stops accepting and waits for the requests in flight; an idle keep-alive " +
|
|
94
|
+
"connection is closed by µWS through uwsOptions.idleTimeout, not by node."
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Nest's json and urlencoded parsers, added once however often this is called.
|
|
101
|
+
*
|
|
102
|
+
* Express answers "are they there already" by scanning `app.router.stack` for a layer whose
|
|
103
|
+
* handler is named `jsonParser` or `urlencodedParser`. There is no such array here, so the scan
|
|
104
|
+
* answered no every time and a second call put a second pair in front of every request.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} [prefix]
|
|
107
|
+
* @param {boolean} [rawBody]
|
|
108
|
+
* @returns {void}
|
|
109
|
+
*/
|
|
110
|
+
registerParserMiddleware(prefix, rawBody) {
|
|
111
|
+
if (this._parsersRegistered) return;
|
|
112
|
+
this._parsersRegistered = true;
|
|
113
|
+
super.registerParserMiddleware(prefix, rawBody);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// a named export and nothing else: `import { FulmineExpressAdapter } from "fulmine.js/nest"`, which
|
|
118
|
+
// is how @nestjs/platform-express exports ExpressAdapter too
|
|
119
|
+
module.exports = { FulmineExpressAdapter };
|
package/src/request.js
CHANGED
|
@@ -152,6 +152,51 @@ const discardedDuplicates = new Set([
|
|
|
152
152
|
// 128 KB of body buffered before uWS is asked to pause
|
|
153
153
|
const READABLE_OPTIONS = { highWaterMark: 128 * 1024 };
|
|
154
154
|
|
|
155
|
+
// The methods node's parser accepts, which is the set a request can arrive with behind Express and
|
|
156
|
+
// the set a route can be registered for here. µWS accepts any token, so without this a line like
|
|
157
|
+
// `{"a":1}GET /path HTTP/1.1` is a request to it, with `{"A":1}GET` as the method. See _mustRefuse.
|
|
158
|
+
const KNOWN_METHODS = new Set(require("http").METHODS);
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Whether a transfer-encoding leaves the body's length knowable, which is RFC 9112's rule that
|
|
162
|
+
* `chunked` comes last. `gzip, chunked` is fine and `chunked, gzip` is not: with a coding applied
|
|
163
|
+
* after the framing one, nothing can say where the body ends, and node answers 400 rather than
|
|
164
|
+
* guess. µWS guesses, and what it guesses wrong becomes the next request on the connection.
|
|
165
|
+
*
|
|
166
|
+
* Read per header rather than over the joined value, so a request splitting the list across two
|
|
167
|
+
* transfer-encoding headers is refused even when the codings would be legal joined up. That is
|
|
168
|
+
* stricter than node by a hair, on a shape nothing sends, and stricter is the safe direction here.
|
|
169
|
+
*
|
|
170
|
+
* @param {string} value one transfer-encoding header, as uWS hands it over
|
|
171
|
+
* @returns {boolean}
|
|
172
|
+
*/
|
|
173
|
+
function endsWithChunked(value) {
|
|
174
|
+
const last = value.slice(value.lastIndexOf(",") + 1).trim();
|
|
175
|
+
// a coding may carry parameters, which are not part of its name
|
|
176
|
+
const semicolon = last.indexOf(";");
|
|
177
|
+
return (semicolon === -1 ? last : last.slice(0, semicolon)).trim().toLowerCase() === "chunked";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The path of the url a request carries right now, without the query.
|
|
182
|
+
*
|
|
183
|
+
* Express reads it off req.url on every access, so a middleware that assigns req.url is seen by
|
|
184
|
+
* whatever runs next, the callback after it in the same route included: the router only takes a
|
|
185
|
+
* rewrite over at its next hop. The cached field answers while the two agree, which is every read
|
|
186
|
+
* of a request nobody rewrote.
|
|
187
|
+
*
|
|
188
|
+
* @param {any} req
|
|
189
|
+
* @returns {string}
|
|
190
|
+
*/
|
|
191
|
+
function currentPath(req) {
|
|
192
|
+
const url = req.url;
|
|
193
|
+
if (url === req._lastUrl) {
|
|
194
|
+
return req._path;
|
|
195
|
+
}
|
|
196
|
+
const query = url.indexOf("?");
|
|
197
|
+
return query === -1 ? url : url.slice(0, query);
|
|
198
|
+
}
|
|
199
|
+
|
|
155
200
|
/**
|
|
156
201
|
* Whether a content-length is a plain count of bytes, which is the only thing RFC 9112 allows.
|
|
157
202
|
*
|
|
@@ -173,6 +218,12 @@ function isByteCount(value) {
|
|
|
173
218
|
return false;
|
|
174
219
|
}
|
|
175
220
|
}
|
|
221
|
+
// A count nothing can represent is not a count. Node refuses one that overflows, and µWS framed
|
|
222
|
+
// the request as if it had said something else, which put the bytes after it in a request of
|
|
223
|
+
// their own. The length test first, so an ordinary value never parses.
|
|
224
|
+
if (value.length > 15 && Number(value) > Number.MAX_SAFE_INTEGER) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
176
227
|
return true;
|
|
177
228
|
}
|
|
178
229
|
|
|
@@ -317,11 +368,12 @@ module.exports = class Request extends LazyReadable {
|
|
|
317
368
|
/** A bodyless request whose empty end has not been delivered yet, see the constructor. */
|
|
318
369
|
#emptyBody = false;
|
|
319
370
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
371
|
+
// `body` is deliberately not declared here. A class field would put the property on every
|
|
372
|
+
// request, and on Express there is none until a body parser assigns one. `"body" in req` is how
|
|
373
|
+
// a library asks whether the body has already been read, and tRPC's express adapter asks
|
|
374
|
+
// exactly that: answering yes on a request nobody had parsed handed it an undefined body and
|
|
375
|
+
// turned every mutation into "Unexpected end of JSON input". Its type lives in types.d.ts,
|
|
376
|
+
// where the rest of the public request surface is described.
|
|
325
377
|
|
|
326
378
|
/**
|
|
327
379
|
* The response this request arrived with, linked so either reaches the other.
|
|
@@ -363,11 +415,15 @@ module.exports = class Request extends LazyReadable {
|
|
|
363
415
|
if (headerKey.length === 14) {
|
|
364
416
|
// a second content-length whatever it says, and one that is not a count of bytes:
|
|
365
417
|
// both make uWS frame the request differently from what is on the wire, see
|
|
366
|
-
//
|
|
418
|
+
// _mustRefuse and isByteCount
|
|
367
419
|
if (r._sawContentLength || !isByteCount(value)) {
|
|
368
|
-
r.
|
|
420
|
+
r._mustRefuse = true;
|
|
369
421
|
}
|
|
370
422
|
r._sawContentLength = true;
|
|
423
|
+
} else if (!endsWithChunked(value)) {
|
|
424
|
+
// chunked has to be the last coding: anything after it and the length of the body
|
|
425
|
+
// is not knowable, which node answers 400 to and µWS served. See endsWithChunked
|
|
426
|
+
r._mustRefuse = true;
|
|
371
427
|
}
|
|
372
428
|
// saying anything about framing at all, "0" included. A parser that can see a
|
|
373
429
|
// content-length answers about the body it describes, even an empty one: a zero length
|
|
@@ -480,9 +536,9 @@ module.exports = class Request extends LazyReadable {
|
|
|
480
536
|
_sawContentLength;
|
|
481
537
|
|
|
482
538
|
/**
|
|
483
|
-
* Whether
|
|
484
|
-
*
|
|
485
|
-
*
|
|
539
|
+
* Whether this request must not be routed at all. Node's parser refuses each of these outright
|
|
540
|
+
* and answers 400; every one of them is a way for bytes the client did not send as a request to
|
|
541
|
+
* be served as one, which is request smuggling.
|
|
486
542
|
*
|
|
487
543
|
* a repeated content-length uWS frames on the first and drops the rest, so a proxy in
|
|
488
544
|
* front reading the last one instead forwards bytes uWS then
|
|
@@ -491,13 +547,17 @@ module.exports = class Request extends LazyReadable {
|
|
|
491
547
|
* included, and frames the request as carrying no body at all,
|
|
492
548
|
* which turns the body the client sent into that same second
|
|
493
549
|
* request. See isByteCount
|
|
550
|
+
* a method nobody defines uWS takes any token as the method, so anything at all
|
|
551
|
+
* followed by a space and a path is a request line to it. A
|
|
552
|
+
* request with no content-length and no transfer-encoding has
|
|
553
|
+
* no body, so the bytes after it are the next request: node
|
|
554
|
+
* reads them and answers 400, uWS served them. See KNOWN_METHODS
|
|
494
555
|
*
|
|
495
|
-
*
|
|
496
|
-
* for the same reason as rawIp.
|
|
556
|
+
* Declared for the same reason as rawIp.
|
|
497
557
|
*
|
|
498
558
|
* @type {boolean|undefined}
|
|
499
559
|
*/
|
|
500
|
-
|
|
560
|
+
_mustRefuse;
|
|
501
561
|
|
|
502
562
|
/**
|
|
503
563
|
* Whether the client asked for the connection to be closed. Declared for the same reason.
|
|
@@ -567,7 +627,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
567
627
|
// A content-length of "0" declares no body and used to stay on the cheap side, but
|
|
568
628
|
// getHeader only ever returns the first of a repeated header, so a duplicate cannot be
|
|
569
629
|
// seen from here, and a duplicate has to be refused rather than routed: see
|
|
570
|
-
//
|
|
630
|
+
// _mustRefuse. Anything that says a word about framing takes the full copy instead.
|
|
571
631
|
//
|
|
572
632
|
// One shape stays invisible here, a content-length present with an empty value: uWS
|
|
573
633
|
// answers "" for that and for a header that was never sent, and nothing in its API
|
|
@@ -632,7 +692,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
632
692
|
}
|
|
633
693
|
if (preset) {
|
|
634
694
|
// the registration's constants: two native crossings and their strings not asked for
|
|
635
|
-
this.
|
|
695
|
+
this._path = preset.path;
|
|
636
696
|
this.originalUrl = preset.path + this.urlQuery;
|
|
637
697
|
this.url = this.originalUrl;
|
|
638
698
|
this._lastUrl = this.originalUrl;
|
|
@@ -646,17 +706,27 @@ module.exports = class Request extends LazyReadable {
|
|
|
646
706
|
// getUrl() is the path already, so the query is joined on and then not split off
|
|
647
707
|
// again. Building originalUrl and picking the path back out of it with indexOf and
|
|
648
708
|
// substring was a search and a second string for something uWS had just handed over.
|
|
649
|
-
this.
|
|
650
|
-
this.originalUrl = this.
|
|
709
|
+
this._path = req.getUrl();
|
|
710
|
+
this.originalUrl = this._path + this.urlQuery;
|
|
651
711
|
this.url = this.originalUrl;
|
|
652
712
|
// what the router last wrote to req.url. A middleware assigning something else is a
|
|
653
713
|
// rewrite, which express honours, and dispatch compares against this to notice it
|
|
654
714
|
this._lastUrl = this.originalUrl;
|
|
655
715
|
// charCodeAt rather than indexing: s[i] builds a one character string to throw away
|
|
656
|
-
this.endsWithSlash = this.
|
|
657
|
-
this._opPath = this.
|
|
658
|
-
this._originalPath = this.
|
|
659
|
-
|
|
716
|
+
this.endsWithSlash = this._path.charCodeAt(this._path.length - 1) === 0x2f;
|
|
717
|
+
this._opPath = this._path;
|
|
718
|
+
this._originalPath = this._path;
|
|
719
|
+
const rawMethod = req.getCaseSensitiveMethod();
|
|
720
|
+
this.method = rawMethod.toUpperCase();
|
|
721
|
+
// node's parser knows a fixed set and refuses everything else; µWS takes the token as
|
|
722
|
+
// it finds it, so a request line is anything with a space in it. Compared before the
|
|
723
|
+
// uppercasing on purpose: a method is case sensitive, node refuses "post", and µWS
|
|
724
|
+
// folds it to POST and serves it. Only asked of a method the framework cannot route
|
|
725
|
+
// anyway, since a route can only be registered for one of these, see the loop that
|
|
726
|
+
// builds the verb methods at the end of router.js
|
|
727
|
+
if (!KNOWN_METHODS.has(rawMethod)) {
|
|
728
|
+
this._mustRefuse = true;
|
|
729
|
+
}
|
|
660
730
|
this._isOptions = this.method === "OPTIONS";
|
|
661
731
|
this._isHead = this.method === "HEAD";
|
|
662
732
|
}
|
|
@@ -1004,6 +1074,17 @@ module.exports = class Request extends LazyReadable {
|
|
|
1004
1074
|
return index !== -1 ? header.slice(0, index).trim() : header.trim();
|
|
1005
1075
|
}
|
|
1006
1076
|
|
|
1077
|
+
/**
|
|
1078
|
+
* The path of the current url, without the query and relative to the mount the request is in.
|
|
1079
|
+
* A getter rather than a field, because express recomputes it from req.url on every read, see
|
|
1080
|
+
* currentPath.
|
|
1081
|
+
*
|
|
1082
|
+
* @returns {string}
|
|
1083
|
+
*/
|
|
1084
|
+
get path() {
|
|
1085
|
+
return currentPath(this);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1007
1088
|
/**
|
|
1008
1089
|
* Takes over what a middleware assigned to req.url: the remaining routing matches the new
|
|
1009
1090
|
* path, and req.query reflects the new query string. The assigned url is relative to the
|
|
@@ -1025,7 +1106,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
1025
1106
|
// a rewrite to "/a?" keeps its "?", as one arriving that way does
|
|
1026
1107
|
this.urlQuery = queryIndex === -1 ? "" : "?" + this._rawQuery;
|
|
1027
1108
|
this._originalPath = prefix + newPath;
|
|
1028
|
-
this.
|
|
1109
|
+
this._path = newPath;
|
|
1029
1110
|
this.endsWithSlash = newPath.charCodeAt(newPath.length - 1) === 0x2f;
|
|
1030
1111
|
this._opPath = newPath;
|
|
1031
1112
|
this._opPathLower = null;
|
package/src/response.js
CHANGED
|
@@ -589,9 +589,16 @@ module.exports = class Response extends LazyWritable {
|
|
|
589
589
|
*
|
|
590
590
|
* Nothing is written here despite the name: the headers go out when the body does.
|
|
591
591
|
*
|
|
592
|
+
* Every header goes through setHeader and not through set. This is node's method, not
|
|
593
|
+
* Express's: Express does not override it, so a content-type given here keeps the value it was
|
|
594
|
+
* given, where `res.set("content-type", "text/html")` would have a charset appended to it. A
|
|
595
|
+
* handler that builds its own response and writes it with writeHead is how every meta-framework
|
|
596
|
+
* on top of Express answers, @astrojs/node and @sveltejs/adapter-node included, so the charset
|
|
597
|
+
* was being added to pages nobody asked it for.
|
|
598
|
+
*
|
|
592
599
|
* @param {number} statusCode
|
|
593
|
-
* @param {string|Record<string, any
|
|
594
|
-
* @param {Record<string, any
|
|
600
|
+
* @param {string|Record<string, any>|any[]} [statusMessage] the reason phrase, or the headers
|
|
601
|
+
* @param {Record<string, any>|any[]} [headers]
|
|
595
602
|
* @returns {this}
|
|
596
603
|
*/
|
|
597
604
|
writeHead(statusCode, statusMessage, headers) {
|
|
@@ -605,8 +612,22 @@ module.exports = class Response extends LazyWritable {
|
|
|
605
612
|
// string reaching here was already taken as the phrase above and simply has no keys.
|
|
606
613
|
headers = /** @type {Record<string, any>} */ (statusMessage);
|
|
607
614
|
}
|
|
615
|
+
if (Array.isArray(headers)) {
|
|
616
|
+
// node takes a flat list here, name then value, and not a list of pairs. An odd length
|
|
617
|
+
// is the caller's mistake and node names the argument in what it throws
|
|
618
|
+
if (headers.length % 2 !== 0) {
|
|
619
|
+
/** @type {NodeJS.ErrnoException} */
|
|
620
|
+
const err = new TypeError(`The argument 'headers' is invalid. Received ${JSON.stringify(headers)}`);
|
|
621
|
+
err.code = "ERR_INVALID_ARG_VALUE";
|
|
622
|
+
throw err;
|
|
623
|
+
}
|
|
624
|
+
for (let i = 0; i < headers.length; i += 2) {
|
|
625
|
+
this.setHeader(headers[i], headers[i + 1]);
|
|
626
|
+
}
|
|
627
|
+
return this;
|
|
628
|
+
}
|
|
608
629
|
for (const header in headers) {
|
|
609
|
-
this.
|
|
630
|
+
this.setHeader(header, headers[header]);
|
|
610
631
|
}
|
|
611
632
|
return this;
|
|
612
633
|
}
|