@nxtedition/lib 21.0.19 → 21.1.1
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 +182 -6
- 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
|
|
|
@@ -30,6 +33,175 @@ function onTimeout() {
|
|
|
30
33
|
this.destroy((timeoutError ??= new createError.RequestTimeout()))
|
|
31
34
|
}
|
|
32
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
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
33
205
|
export async function request(ctx, next) {
|
|
34
206
|
const { req, res, logger } = ctx
|
|
35
207
|
const startTime = performance.now()
|
|
@@ -65,12 +237,16 @@ export async function request(ctx, next) {
|
|
|
65
237
|
reqLogger.debug('request started')
|
|
66
238
|
}
|
|
67
239
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
249
|
+
}
|
|
74
250
|
|
|
75
251
|
const elapsedTime = Math.ceil(performance.now() - startTime)
|
|
76
252
|
|