@nxtedition/lib 21.0.19 → 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 +182 -6
  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
 
@@ -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
- await Promise.all([
69
- next(),
70
- new Promise((resolve, reject) => {
71
- res.on('timeout', onTimeout).on('error', reject).on('close', resolve)
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/lib",
3
- "version": "21.0.19",
3
+ "version": "21.1.0",
4
4
  "license": "MIT",
5
5
  "author": "Robert Nagy <robert.nagy@boffins.se>",
6
6
  "type": "module",