fulmine.js 5.1.0 → 5.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fulmine.js",
3
- "version": "5.1.0",
3
+ "version": "5.1.1",
4
4
  "description": "Drop-in Express 5 replacement on uWebSockets.js. Your existing middleware keeps working.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -388,7 +388,11 @@ class Application extends Router {
388
388
  const response = request.res;
389
389
 
390
390
  try {
391
- const matchedRoute = await this._routeRequest(request, response);
391
+ const routed = this._routeRequest(request, response);
392
+ // dispatch has run its synchronous stretch inside _routeRequest by now, still
393
+ // under the cork uWS holds for this callback; the await below leaves it
394
+ response._corkNeeded = true;
395
+ const matchedRoute = await routed;
392
396
  if (!matchedRoute && !response.headersSent && !response.aborted) {
393
397
  this._endUnmatched(request, response);
394
398
  }
package/src/request.js CHANGED
@@ -140,7 +140,8 @@ module.exports = class Request extends Readable {
140
140
 
141
141
  // Flat, name then value: an array of pairs meant one array allocated per header on every
142
142
  // request, and a request carries eight or ten of them. Everything that reads this walks it two
143
- // at a time.
143
+ // at a time. The names are lowercase by contract: uWS lowers them on the wire and the node
144
+ // shim lowers them in its forEach, so readers compare without lowering again.
144
145
  #rawHeadersEntries = [];
145
146
 
146
147
  /** @type {string|undefined|null} */
@@ -665,10 +666,10 @@ module.exports = class Request extends Readable {
665
666
  const entries = this.#rawHeadersEntries;
666
667
  for (let i = 0, len = entries.length; i < len; i += 2) {
667
668
  const key = entries[i];
668
- // 'if-none-match'.length === 13, 'if-modified-since'.length === 17
669
+ // 'if-none-match'.length === 13, 'if-modified-since'.length === 17; the
670
+ // entries carry lowercase names by contract, so this compares them as they are
669
671
  if (key.length === 13 || key.length === 17) {
670
- const lower = key.toLowerCase();
671
- if (lower === "if-none-match" || lower === "if-modified-since") {
672
+ if (key === "if-none-match" || key === "if-modified-since") {
672
673
  hasConditional = true;
673
674
  break;
674
675
  }
@@ -852,7 +853,8 @@ module.exports = class Request extends Readable {
852
853
  const entries = this.#rawHeadersEntries;
853
854
  for (let index = 0, len = entries.length; index < len; index += 2) {
854
855
  const value = entries[index + 1];
855
- const key = entries[index].toLowerCase();
856
+ // lowercase by the entries' contract, see the field declaration
857
+ const key = entries[index];
856
858
  if (Object.hasOwn(headers, key)) {
857
859
  if (discardedDuplicates.has(key)) {
858
860
  continue;
package/src/response.js CHANGED
@@ -175,6 +175,9 @@ module.exports = class Response extends Writable {
175
175
  }
176
176
 
177
177
  this.body = undefined;
178
+ // false while the uWS route handler is still in its synchronous window, where uWS holds
179
+ // the socket corked itself; the two uWS entry points flip it once that window closes
180
+ this._corkNeeded = false;
178
181
  // shared methods, not arrows: two closures and a once() wrapper here were four
179
182
  // allocations per request. EventEmitter calls listeners with this = the emitter.
180
183
  this.on("error", this._onAbortError);
@@ -272,8 +275,11 @@ module.exports = class Response extends Writable {
272
275
  if (!this.headersSent) {
273
276
  this.writeHead(this.statusCode);
274
277
  // "unknown" and not the bare number: node writes that reason phrase for a code it
275
- // has no message for, so the raw status lines match
276
- this._res.writeStatus(statusLine(this.statusCode, this.statusText));
278
+ // has no message for, so the raw status lines match. The default 200 with no
279
+ // phrase is uWS's own head, byte for byte, so it is not written at all
280
+ if (this.statusCode !== 200 || this.statusText !== undefined) {
281
+ this._res.writeStatus(statusLine(this.statusCode, this.statusText));
282
+ }
277
283
  this.writeHeaders(typeof chunk === "string");
278
284
  }
279
285
 
@@ -475,47 +481,67 @@ module.exports = class Response extends Writable {
475
481
  return this;
476
482
  }
477
483
  this.writeHead(this.statusCode);
478
- this._res.cork(() => {
479
- if (!this.headersSent) {
480
- // freshness is not decided here. node's end() knows nothing about conditional
481
- // requests, and Express answers 304 from send() and from sendFile(), each of
482
- // which strips the entity headers first. Deciding it here meant res.end("body")
483
- // answered 304 and dropped the body that the caller had just written.
484
- // "unknown" for a code without a message, as node's status line has it
484
+ // uWS holds the socket corked for the synchronous window of its route handler, and
485
+ // cork inside cork is a passthrough: the wrapper and its closure are only paid once the
486
+ // answer has outlived that window, which is what _corkNeeded records
487
+ if (this._corkNeeded) {
488
+ this._res.cork(() => this._finish(data, cb));
489
+ } else {
490
+ this._finish(data, cb);
491
+ }
492
+ return this;
493
+ }
494
+
495
+ /**
496
+ * The corked tail of end(): status, headers, body and the finish events. Split out so a
497
+ * synchronous answer calls it straight, already inside uWS's own cork.
498
+ *
499
+ * @param {any} data
500
+ * @param {any} cb
501
+ */
502
+ _finish(data, cb) {
503
+ if (!this.headersSent) {
504
+ // freshness is not decided here. node's end() knows nothing about conditional
505
+ // requests, and Express answers 304 from send() and from sendFile(), each of
506
+ // which strips the entity headers first. Deciding it here meant res.end("body")
507
+ // answered 304 and dropped the body that the caller had just written.
508
+ // "unknown" for a code without a message, as node's status line has it.
509
+ // The default 200 with no phrase is not written at all: uWS emits the identical
510
+ // "HTTP/1.1 200 OK" head on its own, and the crossing costs more than it says
511
+ if (this.statusCode !== 200 || this.statusText !== undefined) {
485
512
  this._res.writeStatus(statusLine(this.statusCode, this.statusText));
486
- this.writeHeaders(true);
487
513
  }
488
- const contentLength = this.headers["content-length"];
489
- if (STATUSES_WITHOUT_BODY.has(this.statusCode) || this.statusCode < 200) {
490
- // no body and no length describing one, whatever the caller passed. node decides
491
- // this the same way, from the status alone, so res.status(304).end("x") sends the
492
- // status and nothing else on either.
493
- this._res.endWithoutBody();
494
- } else if (!data && contentLength) {
495
- this._res.endWithoutBody(contentLength.toString());
514
+ this.writeHeaders(true);
515
+ }
516
+ const contentLength = this.headers["content-length"];
517
+ if (STATUSES_WITHOUT_BODY.has(this.statusCode) || this.statusCode < 200) {
518
+ // no body and no length describing one, whatever the caller passed. node decides
519
+ // this the same way, from the status alone, so res.status(304).end("x") sends the
520
+ // status and nothing else on either.
521
+ this._res.endWithoutBody();
522
+ } else if (!data && contentLength) {
523
+ this._res.endWithoutBody(contentLength.toString());
524
+ } else {
525
+ if (data instanceof Buffer) {
526
+ data = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
527
+ }
528
+ if (this.req.method === "HEAD") {
529
+ const length = Buffer.byteLength(data ?? "");
530
+ this._res.endWithoutBody(length.toString());
496
531
  } else {
497
- if (data instanceof Buffer) {
498
- data = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
499
- }
500
- if (this.req.method === "HEAD") {
501
- const length = Buffer.byteLength(data ?? "");
502
- this._res.endWithoutBody(length.toString());
503
- } else {
504
- this._res.end(data);
505
- }
532
+ this._res.end(data);
506
533
  }
534
+ }
507
535
 
508
- this.finished = true;
509
- this.#socket?.emit("close");
510
- this.emit("finish");
511
- this.emit("close");
512
- cb &&
513
- queueMicrotask(() => {
514
- this.#ended = true;
515
- cb();
516
- });
517
- });
518
- return this;
536
+ this.finished = true;
537
+ this.#socket?.emit("close");
538
+ this.emit("finish");
539
+ this.emit("close");
540
+ cb &&
541
+ queueMicrotask(() => {
542
+ this.#ended = true;
543
+ cb();
544
+ });
519
545
  }
520
546
 
521
547
  /**
package/src/router.js CHANGED
@@ -1408,6 +1408,10 @@ module.exports = class Router extends EventEmitter {
1408
1408
  } catch (err) {
1409
1409
  // what a throw inside a promise executor did: reject, once
1410
1410
  nativeFail.call(walk, err);
1411
+ } finally {
1412
+ // whatever runs after this line is outside the cork uWS held for this
1413
+ // callback, so later writes have to open their own
1414
+ response._corkNeeded = true;
1411
1415
  }
1412
1416
  };
1413
1417
  };