@nxtedition/lib 21.0.18 → 21.1.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.
Files changed (2) hide show
  1. package/http.js +191 -18
  2. package/package.json +1 -1
package/http.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import http2 from 'node:http2'
2
+ import assert from 'node:assert'
2
3
  import createError from 'http-errors'
3
4
  import { performance } from 'perf_hooks'
4
5
  import requestTarget from 'request-target'
@@ -8,6 +9,8 @@ import http from 'http'
8
9
  import fp from 'lodash/fp.js'
9
10
  import tp from 'timers/promises'
10
11
 
12
+ const kAbortController = Symbol('abortController')
13
+
11
14
  const ERR_HEADER_EXPR =
12
15
  /^(content-length|content-type|te|host|upgrade|trailers|connection|keep-alive|http2-settings|transfer-encoding|proxy-connection|proxy-authenticate|proxy-authorization)$/i
13
16
 
@@ -24,22 +27,186 @@ export function genReqId() {
24
27
  return `req-${nextReqId.toString(36)}`
25
28
  }
26
29
 
27
- let reqTimeoutError
28
- let resTimeoutError
30
+ let timeoutError
29
31
 
30
- function onRequestTimeout() {
31
- this.destroy((reqTimeoutError ??= new createError.RequestTimeout()))
32
+ function onTimeout() {
33
+ this.destroy((timeoutError ??= new createError.RequestTimeout()))
32
34
  }
33
35
 
34
- function onResponseTimeout() {
35
- this.destroy((resTimeoutError ??= new createError.RequestTimeout()))
36
+ export class Context {
37
+ #id
38
+ #req
39
+ #res
40
+ #ac
41
+ #url
42
+ #logger
43
+ #headers
44
+ #query
45
+
46
+ constructor(req, res, logger) {
47
+ assert(req)
48
+ assert(res)
49
+ assert(logger)
50
+
51
+ this.#id = req.headers['request-id'] || genReqId()
52
+ this.#req = req
53
+ this.#res = res
54
+ this.#logger = logger.child({ req: { id: req.id, url: req.url } })
55
+ this.#headers = {
56
+ 'request-id': this.#id,
57
+ }
58
+ }
59
+
60
+ setHeader(name, value) {
61
+ this.#headers[name.toLowerCase()] = value
62
+ }
63
+
64
+ removeHeader(name) {
65
+ delete this.#headers[name.toLowerCase()]
66
+ }
67
+
68
+ get [kAbortController]() {
69
+ return this.#ac
70
+ }
71
+
72
+ get signal() {
73
+ this.#ac ??= new AbortController()
74
+ return this.#ac.signal
75
+ }
76
+
77
+ get url() {
78
+ this.#url ??= requestTarget(this.#req)
79
+ return this.#url
80
+ }
81
+
82
+ set url(value) {
83
+ this.#req.url = value
84
+ this.#url = null
85
+ }
86
+
87
+ get query() {
88
+ this.#query ??= this.url.search.length > 1 ? querystring.parse(this.url.search.slice(1)) : {}
89
+ return this.#query
90
+ }
91
+
92
+ get logger() {
93
+ return this.#logger
94
+ }
95
+
96
+ get req() {
97
+ return this.#req
98
+ }
99
+
100
+ get res() {
101
+ return this.#res
102
+ }
103
+
104
+ get method() {
105
+ return this.#req.method
106
+ }
107
+
108
+ get headers() {
109
+ this.#headers ??= {}
110
+ return this.#headers
111
+ }
112
+ }
113
+
114
+ export async function request2(ctx, next) {
115
+ const { req, res, logger } = ctx
116
+ const startTime = performance.now()
117
+
118
+ try {
119
+ if (req.method === 'GET' || req.method === 'HEAD') {
120
+ req.resume() // Dump the body if there is one.
121
+ }
122
+
123
+ const isHealthcheck = req.url === '/healthcheck' || req.url === '/_up'
124
+
125
+ if (!isHealthcheck) {
126
+ logger.debug({ req }, 'request started')
127
+ }
128
+
129
+ const thenable = next()
130
+
131
+ if (thenable?.then) {
132
+ await thenable
133
+ }
134
+
135
+ const elapsedTime = Math.ceil(performance.now() - startTime)
136
+
137
+ if (isHealthcheck) {
138
+ // Do nothing...
139
+ } else if (!res.writableEnded) {
140
+ logger.debug({ res, elapsedTime }, 'request aborted')
141
+ } else if (res.statusCode >= 500) {
142
+ logger.error({ res, elapsedTime }, 'request error')
143
+ } else if (res.statusCode >= 400) {
144
+ logger.warn({ res, elapsedTime }, 'request failed')
145
+ } else {
146
+ logger.debug({ res, elapsedTime }, 'request completed')
147
+ }
148
+ } catch (err) {
149
+ ctx[kAbortController]?.abort(err)
150
+
151
+ const statusCode = err.statusCode || err.$metadata?.httpStatusCode || 500
152
+ const responseTime = Math.ceil(performance.now() - startTime)
153
+
154
+ if (!res.headersSent && !res.destroyed) {
155
+ res.statusCode = statusCode
156
+
157
+ let reqId = req?.id || err.id
158
+ for (const name of res.getHeaderNames()) {
159
+ if (!reqId && name === 'request-id') {
160
+ reqId = res.getHeader(name)
161
+ }
162
+ res.removeHeader(name)
163
+ }
164
+
165
+ if (reqId) {
166
+ res.setHeader('request-id', reqId)
167
+ }
168
+
169
+ if (fp.isPlainObject(err.headers)) {
170
+ for (const [key, val] of Object.entries(err.headers)) {
171
+ if (!ERR_HEADER_EXPR.test(key)) {
172
+ res.setHeader(key, val)
173
+ }
174
+ }
175
+ }
176
+
177
+ if (fp.isPlainObject(err.body)) {
178
+ res.setHeader('content-type', 'application/json')
179
+ res.write(JSON.stringify(err.body))
180
+ }
181
+
182
+ if (statusCode < 500) {
183
+ logger.warn({ req, res, err, responseTime }, 'request failed')
184
+ } else {
185
+ logger.error({ req, res, err, responseTime }, 'request error')
186
+ }
187
+
188
+ res.end()
189
+ } else {
190
+ if (req.aborted || !res.writableEnded || err.name === 'AbortError') {
191
+ logger.debug({ req, res, err, responseTime }, 'request aborted')
192
+ } else if (statusCode < 500) {
193
+ logger.warn({ req, res, err, responseTime }, 'request failed')
194
+ } else {
195
+ logger.error({ req, res, err, responseTime }, 'request error')
196
+ }
197
+
198
+ if (!res.writableEnded) {
199
+ res.destroy()
200
+ }
201
+ }
202
+ }
36
203
  }
37
204
 
38
205
  export async function request(ctx, next) {
39
206
  const { req, res, logger } = ctx
40
207
  const startTime = performance.now()
41
208
 
42
- const ac = new AbortController()
209
+ const ac = ctx.signal !== undefined ? null : new AbortController()
43
210
 
44
211
  let reqLogger = logger
45
212
  try {
@@ -49,11 +216,14 @@ export async function request(ctx, next) {
49
216
  }
50
217
 
51
218
  ctx.id = req.id = res.id = req.headers['request-id'] || genReqId()
52
- ctx.signal = ac.signal
53
219
  ctx.method = req.method
54
220
  ctx.query = ctx.url.search.length > 1 ? querystring.parse(ctx.url.search.slice(1)) : {}
55
221
  ctx.logger = req.log = res.log = logger.child({ req: { id: req.id, url: req.url } })
56
222
 
223
+ if (ac) {
224
+ ctx.signal = ac.signal
225
+ }
226
+
57
227
  if (req.method === 'GET' || req.method === 'HEAD') {
58
228
  req.resume() // Dump the body if there is one.
59
229
  }
@@ -67,16 +237,15 @@ export async function request(ctx, next) {
67
237
  reqLogger.debug('request started')
68
238
  }
69
239
 
70
- try {
71
- await Promise.all([
72
- next(),
73
- new Promise((resolve, reject) => {
74
- req.on('timeout', onRequestTimeout).on('error', reject)
75
- res.on('timeout', onResponseTimeout).on('error', reject).on('close', resolve)
76
- }),
77
- ])
78
- } finally {
79
- ac.abort()
240
+ const nextPromise = next()
241
+ const responsePromise = new Promise((resolve, reject) => {
242
+ res.on('timeout', onTimeout).on('error', reject).on('close', resolve)
243
+ })
244
+
245
+ if (!nextPromise) {
246
+ await Promise.all([nextPromise, responsePromise])
247
+ } else if (!res.closed) {
248
+ await responsePromise
80
249
  }
81
250
 
82
251
  const elapsedTime = Math.ceil(performance.now() - startTime)
@@ -93,6 +262,8 @@ export async function request(ctx, next) {
93
262
  reqLogger.debug({ res, elapsedTime }, 'request completed')
94
263
  }
95
264
  } catch (err) {
265
+ ac?.abort(err)
266
+
96
267
  const statusCode = err.statusCode || err.$metadata?.httpStatusCode || 500
97
268
  const responseTime = Math.ceil(performance.now() - startTime)
98
269
 
@@ -380,6 +551,8 @@ export async function upgrade(ctx, next) {
380
551
 
381
552
  reqLogger.debug('stream completed')
382
553
  } catch (err) {
554
+ ac?.abort(err)
555
+
383
556
  const statusCode = err.statusCode || 500
384
557
 
385
558
  if (aborted || err.name === 'AbortError' || err.code === 'ERR_STREAM_PREMATURE_CLOSE') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/lib",
3
- "version": "21.0.18",
3
+ "version": "21.1.0",
4
4
  "license": "MIT",
5
5
  "author": "Robert Nagy <robert.nagy@boffins.se>",
6
6
  "type": "module",