@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.
- package/http.js +191 -18
- 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
|
|
28
|
-
let resTimeoutError
|
|
30
|
+
let timeoutError
|
|
29
31
|
|
|
30
|
-
function
|
|
31
|
-
this.destroy((
|
|
32
|
+
function onTimeout() {
|
|
33
|
+
this.destroy((timeoutError ??= new createError.RequestTimeout()))
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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') {
|