@dooboostore/simple-boot-http-server 1.0.33 → 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.33",
3
+ "version": "1.0.35",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",
@@ -55,16 +55,18 @@
55
55
  "SeoDH <adonis_84@naver.com> (https://github.com/SeoDH)",
56
56
  "tkyung (https://github.com/tkyungDev)",
57
57
  "Hoil-Gang (https://github.com/khl1154)",
58
- "hwangsero (https://github.com/hwangsero)"
58
+ "hwangsero (https://github.com/hwangsero)",
59
+ "visualkhh <visualkhh@gmail.com> (https://github.com/visualkhh)"
59
60
  ],
60
- "homepage": "https://github.com/dooboostore-develop/packages",
61
+ "homepage": "https://dooboostore-develop.github.io/package/simple-boot-http-server",
61
62
  "bugs": {
62
- "url": "https://github.com/dooboostore-develop/packages",
63
+ "url": "https://github.com/dooboostore-develop/packages/issues",
63
64
  "email": "dooboostore@gmail.com"
64
65
  },
65
66
  "repository": {
66
67
  "type": "git",
67
- "url": "https://github.com/dooboostore-develop/packages"
68
+ "url": "git+https://github.com/dooboostore-develop/packages.git",
69
+ "directory": "packages/@dooboostore/simple-boot-http-server"
68
70
  },
69
71
  "peerDependencies": {
70
72
  "reflect-metadata": "^0.2.2"
@@ -98,11 +100,11 @@
98
100
  "mime-types": "^2.1.34",
99
101
  "node-gzip": "^1.1.2",
100
102
  "ws": "^8.18.3",
101
- "@dooboostore/core": "1.0.33",
102
- "@dooboostore/dom-render": "1.0.115",
103
103
  "@dooboostore/core-node": "1.0.14",
104
- "@dooboostore/core-web": "1.0.14",
105
- "@dooboostore/simple-boot": "1.0.60"
104
+ "@dooboostore/dom-render": "1.0.115",
105
+ "@dooboostore/core": "1.0.33",
106
+ "@dooboostore/simple-boot": "1.0.60",
107
+ "@dooboostore/core-web": "1.0.14"
106
108
  },
107
109
  "scripts": {
108
110
  "clean": "rm -rf dist",
@@ -44,6 +44,27 @@ export class SimpleBootHttpServer extends SimpleApplication {
44
44
  this.sessionManager = new SessionManager(this.option);
45
45
  }
46
46
 
47
+ /**
48
+ * Final response flush that can never escape as an unhandled rejection.
49
+ * When the response is already gone (sent, closed, or destroyed mid-write),
50
+ * there is nobody left to send to, so gone-response write errors
51
+ * (ERR_HTTP_HEADERS_SENT, ERR_STREAM_ALREADY_FINISHED,
52
+ * ERR_STREAM_DESTROYED, ECONNRESET) are swallowed; anything else still throws.
53
+ */
54
+ private async safeResEnd(rr: RequestResponse): Promise<void> {
55
+ if (rr.resIsDone()) {
56
+ return;
57
+ }
58
+ try {
59
+ await rr.resEnd();
60
+ } catch (e: any) {
61
+ const code = e && (e.code || '');
62
+ if (code !== 'ERR_HTTP_HEADERS_SENT' && code !== 'ERR_STREAM_ALREADY_FINISHED' && code !== 'ERR_STREAM_DESTROYED' && code !== 'ECONNRESET') {
63
+ throw e;
64
+ }
65
+ }
66
+ }
67
+
47
68
 
48
69
  public run(otherInstanceSim?: Map<ConstructorType<any> | Function | SimConfig | symbol, any>) {
49
70
  otherInstanceSim ??= new Map<ConstructorType<any> | Function | SimConfig | symbol, any>();
@@ -95,7 +116,7 @@ export class SimpleBootHttpServer extends SimpleApplication {
95
116
  }
96
117
 
97
118
  if (!rr.resIsDone()) {
98
- rr.resEnd();
119
+ await this.safeResEnd(rr);
99
120
  }
100
121
  });
101
122
  res.on('error', async () => {
@@ -113,7 +134,7 @@ export class SimpleBootHttpServer extends SimpleApplication {
113
134
  }
114
135
 
115
136
  if (!rr.resIsDone()) {
116
- rr.resEnd();
137
+ await this.safeResEnd(rr);
117
138
  }
118
139
  });
119
140
 
@@ -351,14 +372,18 @@ export class SimpleBootHttpServer extends SimpleApplication {
351
372
  headers[HttpHeaders.ContentType] = contentType;
352
373
  }
353
374
  rr.resSetHeaders(headers);
354
- 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
+ }
355
381
  await rr.resStream(data, { compress: stream?.compress });
356
382
  } else {
357
383
  if (it.config?.resolver) {
358
384
  const execute = typeof it.config.resolver === 'function' ? this.simstanceManager.getOrNewSim({target: it.config.resolver}) : it.config.resolver;
359
385
  data = await execute?.resolve?.(data, rr, resConfig);
360
386
  }
361
- const status = resConfig?.status ?? HttpStatus.Ok;
362
387
  const headers = resConfig?.header ?? {};
363
388
  if (resConfig?.contentType) {
364
389
  headers[HttpHeaders.ContentType] = resConfig.contentType;
@@ -369,7 +394,12 @@ export class SimpleBootHttpServer extends SimpleApplication {
369
394
  data = JSON.stringify(data);
370
395
  }
371
396
  rr.resSetHeaders(headers)
372
- 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
+ }
373
403
  rr.resWrite(data);
374
404
  }
375
405
  }
@@ -419,7 +449,7 @@ export class SimpleBootHttpServer extends SimpleApplication {
419
449
  }
420
450
 
421
451
  if (!rr.resIsDone()) {
422
- rr.resEnd();
452
+ await this.safeResEnd(rr);
423
453
  }
424
454
  });
425
455
 
@@ -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