@dooboostore/simple-boot-http-server 1.0.34 → 1.0.35

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": "@dooboostore/simple-boot-http-server",
3
- "version": "1.0.34",
3
+ "version": "1.0.35",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",
@@ -100,11 +100,11 @@
100
100
  "mime-types": "^2.1.34",
101
101
  "node-gzip": "^1.1.2",
102
102
  "ws": "^8.18.3",
103
+ "@dooboostore/core-node": "1.0.14",
104
+ "@dooboostore/dom-render": "1.0.115",
103
105
  "@dooboostore/core": "1.0.33",
104
- "@dooboostore/core-web": "1.0.14",
105
106
  "@dooboostore/simple-boot": "1.0.60",
106
- "@dooboostore/dom-render": "1.0.115",
107
- "@dooboostore/core-node": "1.0.14"
107
+ "@dooboostore/core-web": "1.0.14"
108
108
  },
109
109
  "scripts": {
110
110
  "clean": "rm -rf dist",
@@ -372,14 +372,18 @@ export class SimpleBootHttpServer extends SimpleApplication {
372
372
  headers[HttpHeaders.ContentType] = contentType;
373
373
  }
374
374
  rr.resSetHeaders(headers);
375
- rr.resSetStatusCode(resConfig?.status ?? HttpStatus.Ok);
375
+ if (resConfig?.status !== undefined) {
376
+ // Explicit decorator status wins. The Node default (200) is left
377
+ // alone otherwise so a handler-set status (e.g. via resEnd(Response))
378
+ // is not clobbered back to 200.
379
+ rr.resSetStatusCode(resConfig.status);
380
+ }
376
381
  await rr.resStream(data, { compress: stream?.compress });
377
382
  } else {
378
383
  if (it.config?.resolver) {
379
384
  const execute = typeof it.config.resolver === 'function' ? this.simstanceManager.getOrNewSim({target: it.config.resolver}) : it.config.resolver;
380
385
  data = await execute?.resolve?.(data, rr, resConfig);
381
386
  }
382
- const status = resConfig?.status ?? HttpStatus.Ok;
383
387
  const headers = resConfig?.header ?? {};
384
388
  if (resConfig?.contentType) {
385
389
  headers[HttpHeaders.ContentType] = resConfig.contentType;
@@ -390,7 +394,12 @@ export class SimpleBootHttpServer extends SimpleApplication {
390
394
  data = JSON.stringify(data);
391
395
  }
392
396
  rr.resSetHeaders(headers)
393
- rr.resSetStatusCode(status);
397
+ if (resConfig?.status !== undefined) {
398
+ // An explicit decorator status wins. Otherwise leave the status
399
+ // alone (Node defaults to 200) so a handler-set status is not
400
+ // clobbered back to 200.
401
+ rr.resSetStatusCode(resConfig.status);
402
+ }
394
403
  rr.resWrite(data);
395
404
  }
396
405
  }
@@ -18,6 +18,14 @@ import { Blob } from 'node:buffer';
18
18
  import * as fs from 'fs';
19
19
  import * as os from 'os';
20
20
  import { RandomUtils } from '@dooboostore/core';
21
+
22
+ /**
23
+ * Responses with a send already in flight. Keyed by the underlying
24
+ * ServerResponse (shared by RequestResponse and its chains), so concurrent
25
+ * resEnd() calls for one response serialize instead of racing each other
26
+ * through gzip/setHeader into ERR_HTTP_HEADERS_SENT.
27
+ */
28
+ const resEndingSet = new WeakSet<object>();
21
29
  import { FileUtils } from '@dooboostore/core-node';
22
30
  import { ConvertUtils as CoreConvertUtils } from '@dooboostore/core';
23
31
  import { ConvertUtils } from '@dooboostore/core-node';
@@ -491,6 +499,12 @@ export class RequestResponse {
491
499
  }
492
500
 
493
501
  resWrite(chunk: string | Buffer | any, encoding: BufferEncoding = 'utf8') {
502
+ if (chunk === undefined) {
503
+ // A handler that already wrote via rr.resEnd()/resWrite() and returns
504
+ // undefined must not have its buffered body clobbered by the pipeline's
505
+ // return-value write (express#7510-class probe caught this).
506
+ return this.createRequestResponseChain(this.resWriteChunk);
507
+ }
494
508
  this.resWriteChunk = chunk;
495
509
  return this.createRequestResponseChain(this.resWriteChunk);
496
510
  }
@@ -532,32 +546,52 @@ export class RequestResponse {
532
546
  }
533
547
 
534
548
  async resEnd(chunk?: string | Buffer | Blob | ArrayBuffer | Response) {
535
- if (chunk instanceof Response) {
536
- chunk.headers.forEach((value, key) => {
537
- this.res.setHeader(`ORIGIN-${key}`, value);
538
- });
539
- if (!this.resHeader(HttpHeaders.ContentType)) {
540
- const contentType = chunk.headers.get(HttpHeaders.ContentType);
541
- if (contentType) this.res.setHeader(HttpHeaders.ContentType, contentType);
542
- }
543
- this.res.statusCode = chunk.status;
544
- this.resWriteChunk = Buffer.from(await chunk.arrayBuffer());
545
- } else if (chunk instanceof Blob) {
546
- this.resWriteChunk = Buffer.from(await chunk.arrayBuffer());
547
- if (!this.resHeader(HttpHeaders.ContentType)) {
548
- this.res.setHeader(HttpHeaders.ContentType, chunk.type);
549
- }
550
- } else if (chunk instanceof ArrayBuffer) {
551
- this.resWriteChunk = Buffer.from(chunk);
552
- } else {
553
- this.resWriteChunk = chunk ?? this.resWriteChunk;
549
+ if (resEndingSet.has(this.res) || this.resIsDone()) {
550
+ // A send is already in flight (or finished): concurrent resEnd() calls
551
+ // — e.g. a handler-sent body followed by the pipeline's final flush —
552
+ // must not start a second send. Without this guard the two sends race
553
+ // through gzip/setHeader and the loser throws ERR_HTTP_HEADERS_SENT.
554
+ // The set is keyed by the underlying response (shared by chains).
555
+ return;
554
556
  }
557
+ resEndingSet.add(this.res);
558
+ try {
559
+ if (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView(chunk) && !Buffer.isBuffer(chunk)) {
560
+ // Typed-array views (incl. DataView, which Node's res.end() rejects outright)
561
+ // are normalized to their exact byte range. Native res.end() already honors
562
+ // byteOffset for typed arrays, but DataView throws ERR_INVALID_ARG_TYPE,
563
+ // so every non-Buffer view goes through here (expressjs/express#7510 class).
564
+ const view = chunk as unknown as { buffer: ArrayBuffer; byteOffset: number; byteLength: number };
565
+ this.resWriteChunk = Buffer.from(view.buffer, view.byteOffset, view.byteLength);
566
+ } else if (chunk instanceof Response) {
567
+ chunk.headers.forEach((value, key) => {
568
+ this.res.setHeader(`ORIGIN-${key}`, value);
569
+ });
570
+ if (!this.resHeader(HttpHeaders.ContentType)) {
571
+ const contentType = chunk.headers.get(HttpHeaders.ContentType);
572
+ if (contentType) this.res.setHeader(HttpHeaders.ContentType, contentType);
573
+ }
574
+ this.res.statusCode = chunk.status;
575
+ this.resWriteChunk = Buffer.from(await chunk.arrayBuffer());
576
+ } else if (chunk instanceof Blob) {
577
+ this.resWriteChunk = Buffer.from(await chunk.arrayBuffer());
578
+ if (!this.resHeader(HttpHeaders.ContentType)) {
579
+ this.res.setHeader(HttpHeaders.ContentType, chunk.type);
580
+ }
581
+ } else if (chunk instanceof ArrayBuffer) {
582
+ this.resWriteChunk = Buffer.from(chunk);
583
+ } else {
584
+ this.resWriteChunk = chunk ?? this.resWriteChunk;
585
+ }
555
586
 
556
- if (this.req.readable) {
557
- await this.reqBodyData();
558
- await this.resEndChunk();
559
- } else {
560
- await this.resEndChunk();
587
+ if (this.req.readable) {
588
+ await this.reqBodyData();
589
+ await this.resEndChunk();
590
+ } else {
591
+ await this.resEndChunk();
592
+ }
593
+ } finally {
594
+ resEndingSet.delete(this.res);
561
595
  }
562
596
  }
563
597