@jsenv/server 12.2.1 → 12.3.3

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/main.js CHANGED
@@ -42,6 +42,7 @@ export { pluginContentNegotiationCheck } from "./src/content_negotiation/plugin_
42
42
 
43
43
  // others
44
44
  export { fetchUrl } from "./src/fetch_url.js"
45
+ export { fromFetchResponse } from "./src/from_fetch_response.js"
45
46
  export { composeTwoResponses } from "./src/internal/response_composition.js"
46
47
  export { pluginRessourceAliases } from "./src/ressource_aliases/plugin_ressource_aliases.js"
47
48
  export { pluginRequestWaitingCheck } from "./src/plugin_request_waiting_check.js"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/server",
3
- "version": "12.2.1",
3
+ "version": "12.3.3",
4
4
  "description": "Write your Node.js server using pure functions",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -13,7 +13,7 @@
13
13
  "url": "https://github.com/jsenv/server"
14
14
  },
15
15
  "engines": {
16
- "node": ">=16.12.0"
16
+ "node": ">=16.13.0"
17
17
  },
18
18
  "publishConfig": {
19
19
  "access": "public"
@@ -38,33 +38,33 @@
38
38
  "test-with-coverage": "npm run test -- --coverage",
39
39
  "generate-html-snapshots": "node ./test/internal_error/generate_html_snapshot_files.mjs",
40
40
  "prettier": "prettier --write .",
41
- "certificate-authority-install": "node ./script/certificate/certificate_authority_install.mjs",
41
+ "certificate-install": "node ./script/certificate/certificate_install.mjs",
42
42
  "playwright-install": "npx playwright install-deps && npx playwright install"
43
43
  },
44
44
  "dependencies": {
45
45
  "@jsenv/abort": "4.1.2",
46
46
  "@jsenv/logger": "4.0.1",
47
- "@jsenv/url-meta": "6.0.1",
47
+ "@jsenv/url-meta": "6.0.3",
48
48
  "node-fetch": "2.6.2",
49
49
  "path-to-regexp": "6.2.0"
50
50
  },
51
51
  "devDependencies": {
52
- "@jsenv/assert": "2.4.0",
53
- "@jsenv/core": "24.5.0",
52
+ "@jsenv/assert": "2.4.1",
53
+ "@jsenv/core": "25.1.0",
54
54
  "@jsenv/eslint-config": "16.0.9",
55
- "@jsenv/filesystem": "2.6.0",
55
+ "@jsenv/filesystem": "2.6.1",
56
56
  "@jsenv/github-release-package": "1.2.3",
57
57
  "@jsenv/https-local": "1.0.3",
58
- "@jsenv/importmap-eslint-resolver": "5.2.1",
59
- "@jsenv/importmap-node-module": "4.0.1",
58
+ "@jsenv/importmap-eslint-resolver": "5.2.2",
59
+ "@jsenv/importmap-node-module": "5.1.0",
60
60
  "@jsenv/package-publish": "1.6.2",
61
61
  "@jsenv/performance-impact": "2.2.1",
62
- "eslint": "8.4.1",
63
- "eslint-plugin-import": "2.25.3",
62
+ "eslint": "8.7.0",
63
+ "eslint-plugin-import": "2.25.4",
64
64
  "eventsource": "1.1.0",
65
- "fastify": "3.25.0",
65
+ "fastify": "3.25.3",
66
66
  "fastify-static": "4.5.0",
67
- "playwright": "1.17.1",
67
+ "playwright": "1.17.2",
68
68
  "prettier": "2.5.1"
69
69
  }
70
70
  }
@@ -0,0 +1,15 @@
1
+ export const fromFetchResponse = (fetchResponse) => {
2
+ const responseHeaders = {}
3
+ const headersToIgnore = ["connection"]
4
+ fetchResponse.headers.forEach((value, name) => {
5
+ if (!headersToIgnore.includes(name)) {
6
+ responseHeaders[name] = value
7
+ }
8
+ })
9
+ return {
10
+ status: fetchResponse.status,
11
+ statusText: fetchResponse.statusText,
12
+ headers: responseHeaders,
13
+ body: fetchResponse.body, // node-fetch assumed
14
+ }
15
+ }
@@ -1,12 +1,17 @@
1
1
  // https://github.com/jamestalmage/stream-to-observable/blob/master/index.js
2
2
  import { Readable } from "node:stream"
3
-
4
3
  import { createObservable } from "./observable.js"
5
4
 
6
5
  const readableStreamLifetimeInSeconds = 120
7
6
 
8
7
  export const nodeStreamToObservable = (nodeStream) => {
9
8
  const observable = createObservable(({ next, error, complete }) => {
9
+ if (nodeStream.isPaused()) {
10
+ nodeStream.resume()
11
+ } else if (nodeStream.complete) {
12
+ complete()
13
+ return null
14
+ }
10
15
  const cleanup = () => {
11
16
  nodeStream.removeListener("data", next)
12
17
  nodeStream.removeListener("error", error)
@@ -14,13 +19,17 @@ export const nodeStreamToObservable = (nodeStream) => {
14
19
  nodeStream.removeListener("close", cleanup)
15
20
  nodeStream.destroy()
16
21
  }
17
-
18
22
  // should we do nodeStream.resume() in case the stream was paused ?
19
23
  nodeStream.once("error", error)
20
- nodeStream.on("data", next)
21
- nodeStream.once("close", cleanup)
22
- nodeStream.once("end", complete)
23
-
24
+ nodeStream.on("data", (data) => {
25
+ next(data)
26
+ })
27
+ nodeStream.once("close", () => {
28
+ cleanup()
29
+ })
30
+ nodeStream.once("end", () => {
31
+ complete()
32
+ })
24
33
  return cleanup
25
34
  })
26
35
 
@@ -1,3 +1,4 @@
1
+ import http from "node:http"
1
2
  import { Http2ServerResponse } from "node:http2"
2
3
  import { raceCallbacks } from "@jsenv/abort"
3
4
 
@@ -22,8 +23,8 @@ export const populateNodeResponse = async (
22
23
  status,
23
24
  statusText,
24
25
  headers,
26
+ onHeadersSent,
25
27
  })
26
- onHeadersSent()
27
28
 
28
29
  if (!body) {
29
30
  onEnd()
@@ -134,41 +135,48 @@ export const populateNodeResponse = async (
134
135
  })
135
136
  }
136
137
 
137
- const writeHead = (responseStream, { status, statusText, headers }) => {
138
+ const writeHead = (
139
+ responseStream,
140
+ { status, statusText, headers, onHeadersSent },
141
+ ) => {
138
142
  const responseIsHttp2ServerResponse =
139
143
  responseStream instanceof Http2ServerResponse
140
144
  const responseIsServerHttp2Stream =
141
145
  responseStream.constructor.name === "ServerHttp2Stream"
142
-
143
146
  let nodeHeaders = headersToNodeHeaders(headers, {
144
147
  // https://github.com/nodejs/node/blob/79296dc2d02c0b9872bbfcbb89148ea036a546d0/lib/internal/http2/compat.js#L112
145
148
  ignoreConnectionHeader:
146
149
  responseIsHttp2ServerResponse || responseIsServerHttp2Stream,
147
150
  })
148
-
151
+ if (statusText === undefined) {
152
+ statusText = statusTextFromStatus(status)
153
+ }
149
154
  if (responseIsServerHttp2Stream) {
150
155
  nodeHeaders = {
151
156
  ...nodeHeaders,
152
157
  ":status": status,
153
158
  }
154
159
  responseStream.respond(nodeHeaders)
160
+ onHeadersSent({ nodeHeaders, status, statusText })
155
161
  return
156
162
  }
157
-
158
163
  // nodejs strange signature for writeHead force this
159
164
  // https://nodejs.org/api/http.html#http_response_writehead_statuscode_statusmessage_headers
160
165
  if (
161
- statusText === undefined ||
162
166
  // https://github.com/nodejs/node/blob/79296dc2d02c0b9872bbfcbb89148ea036a546d0/lib/internal/http2/compat.js#L97
163
167
  responseIsHttp2ServerResponse
164
168
  ) {
165
169
  responseStream.writeHead(status, nodeHeaders)
170
+ onHeadersSent({ nodeHeaders, status, statusText })
166
171
  return
167
172
  }
168
-
169
173
  responseStream.writeHead(status, statusText, nodeHeaders)
174
+ onHeadersSent({ nodeHeaders, status, statusText })
170
175
  }
171
176
 
177
+ const statusTextFromStatus = (status) =>
178
+ http.STATUS_CODES[status] || "not specified"
179
+
172
180
  const headersToNodeHeaders = (headers, { ignoreConnectionHeader }) => {
173
181
  const nodeHeaders = {}
174
182
 
@@ -230,6 +230,10 @@ export const startServer = async ({
230
230
  })
231
231
 
232
232
  const requestCallback = async (nodeRequest, nodeResponse) => {
233
+ // pause the stream to let a chance to "requestToResponse"
234
+ // to call "requestRequestBody". Without this the request body readable stream
235
+ // might be closed when we'll try to attach "data" and "end" listeners to it
236
+ nodeRequest.pause()
233
237
  if (!nagle) {
234
238
  nodeRequest.connection.setNoDelay(true)
235
239
  }
@@ -309,7 +313,6 @@ export const startServer = async ({
309
313
 
310
314
  visitRequestNodeToLog(rootRequestNode, 0)
311
315
  }
312
-
313
316
  nodeRequest.on("error", (error) => {
314
317
  if (error.message === "aborted") {
315
318
  addRequestLog(rootRequestNode, {
@@ -472,8 +475,8 @@ export const startServer = async ({
472
475
  }
473
476
  }
474
477
  })
475
- let { aborted, error, responseProperties } = responseFromShortcircuit
476
- ? { responseProperties: responseFromShortcircuit }
478
+ const result = responseFromShortcircuit
479
+ ? { returnValue: responseFromShortcircuit }
477
480
  : await generateResponseDescription({
478
481
  requestToResponse,
479
482
  request,
@@ -533,65 +536,72 @@ export const startServer = async ({
533
536
  },
534
537
  })
535
538
 
539
+ const { aborted } = result
536
540
  if (aborted) {
537
541
  return ABORTED_RESPONSE_PROPERTIES
538
542
  }
539
-
540
- // internal error, create 500 response
541
- if (error) {
542
- if (
543
- // stopOnInternalError stops server only if requestToResponse generated
544
- // a non controlled error (internal error).
545
- // if requestToResponse gracefully produced a 500 response (it did not throw)
546
- // then we can assume we are still in control of what we are doing
547
- stopOnInternalError
548
- ) {
549
- // il faudrais pouvoir stop que les autres response ?
550
- stop(STOP_REASON_INTERNAL_ERROR)
551
- }
552
-
553
- const propertiesFromError = await serverInternalErrorToResponse(error, {
554
- request,
555
- sendServerInternalErrorDetails,
556
- })
557
- responseProperties = composeTwoResponses(
558
- {
559
- status: 500,
560
- statusText: statusToStatusText(500),
561
- headers: {
562
- // ensure error are not cached
563
- "cache-control": "no-store",
564
- "content-type": "text/plain",
543
+ const readResponseProperties = async () => {
544
+ const { error } = result
545
+ // internal error, create 500 response
546
+ if (error) {
547
+ if (
548
+ // stopOnInternalError stops server only if requestToResponse generated
549
+ // a non controlled error (internal error).
550
+ // if requestToResponse gracefully produced a 500 response (it did not throw)
551
+ // then we can assume we are still in control of what we are doing
552
+ stopOnInternalError
553
+ ) {
554
+ // il faudrais pouvoir stop que les autres response ?
555
+ stop(STOP_REASON_INTERNAL_ERROR)
556
+ }
557
+ const propertiesFromError = await serverInternalErrorToResponse(
558
+ error,
559
+ {
560
+ request,
561
+ sendServerInternalErrorDetails,
565
562
  },
566
- },
567
- propertiesFromError,
568
- )
569
- addRequestLog(requestNode, {
570
- type: "error",
571
- value: createDetailedMessage(
572
- `internal error while handling request`,
563
+ )
564
+ addRequestLog(requestNode, {
565
+ type: "error",
566
+ value: createDetailedMessage(
567
+ `internal error while handling request`,
568
+ {
569
+ "error stack": error.stack,
570
+ },
571
+ ),
572
+ })
573
+ return composeTwoResponses(
573
574
  {
574
- "error stack": error.stack,
575
+ status: 500,
576
+ statusText: "Internal Server Error",
577
+ headers: {
578
+ // ensure error are not cached
579
+ "cache-control": "no-store",
580
+ "content-type": "text/plain",
581
+ },
575
582
  },
576
- ),
577
- })
578
- } else {
583
+ propertiesFromError,
584
+ )
585
+ }
586
+
579
587
  const {
580
588
  status = 501,
581
- statusText = statusToStatusText(status),
589
+ statusText,
582
590
  headers = {},
583
591
  body,
584
592
  ...rest
585
- } = responseProperties || {}
586
- responseProperties = {
593
+ } = result.returnValue || {}
594
+ const responseProperties = {
587
595
  status,
588
596
  statusText,
589
597
  headers,
590
598
  body,
591
599
  ...rest,
592
600
  }
601
+ return responseProperties
593
602
  }
594
603
 
604
+ let responseProperties = await readResponseProperties()
595
605
  // call every plugin "onResponse" hook
596
606
  requestPlugins.forEach((requestPlugin) => {
597
607
  const { onResponse } = requestPlugin
@@ -605,7 +615,6 @@ export const startServer = async ({
605
615
  }
606
616
  }
607
617
  })
608
-
609
618
  if (
610
619
  request.method !== "HEAD" &&
611
620
  responseProperties.headers["content-length"] > 0 &&
@@ -616,7 +625,6 @@ export const startServer = async ({
616
625
  value: `content-length header is ${responseProperties.headers["content-length"]} but body is empty`,
617
626
  })
618
627
  }
619
-
620
628
  return responseProperties
621
629
  }
622
630
 
@@ -659,12 +667,10 @@ export const startServer = async ({
659
667
  })
660
668
  onRequestHandled(requestNode)
661
669
  },
662
- onHeadersSent: () => {
670
+ onHeadersSent: ({ status, statusText }) => {
663
671
  addRequestLog(requestNode, {
664
672
  type: "info",
665
- value: `${colorizeResponseStatus(responseProperties.status)} ${
666
- responseProperties.statusText
667
- }`,
673
+ value: `${colorizeResponseStatus(status)} ${statusText}`,
668
674
  })
669
675
  },
670
676
  onEnd: () => {
@@ -680,6 +686,7 @@ export const startServer = async ({
680
686
  const responseProperties = await handleRequest(request, {
681
687
  requestNode: rootRequestNode,
682
688
  })
689
+ nodeRequest.resume()
683
690
  if (receiveRequestOperation.signal.aborted) {
684
691
  return
685
692
  }
@@ -759,7 +766,7 @@ const generateResponseDescription = async ({
759
766
  pushResponse,
760
767
  })
761
768
  return {
762
- responseProperties: returnValue,
769
+ returnValue,
763
770
  }
764
771
  } catch (error) {
765
772
  if (error.name === "AbortError" && request.signal.aborted) {
@@ -772,10 +779,6 @@ const generateResponseDescription = async ({
772
779
  }
773
780
  }
774
781
  }
775
-
776
- const statusToStatusText = (status) =>
777
- http.STATUS_CODES[status] || "not specified"
778
-
779
782
  const testCanPushStream = (http2Stream) => {
780
783
  if (!http2Stream.pushAllowed) {
781
784
  return {