koa 3.2.0 → 3.3.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/dist/koa.mjs +2 -0
- package/lib/application.js +32 -11
- package/lib/context.js +7 -1
- package/lib/request.js +22 -8
- package/lib/response.js +20 -6
- package/lib/search-params.js +8 -2
- package/package.json +2 -2
package/dist/koa.mjs
CHANGED
package/lib/application.js
CHANGED
|
@@ -14,7 +14,7 @@ const { AsyncLocalStorage } = require('node:async_hooks')
|
|
|
14
14
|
const onFinished = require('on-finished')
|
|
15
15
|
const compose = require('koa-compose')
|
|
16
16
|
const statuses = require('statuses')
|
|
17
|
-
const
|
|
17
|
+
const createHttpError = require('http-errors')
|
|
18
18
|
|
|
19
19
|
const request = require('./request')
|
|
20
20
|
const response = require('./response')
|
|
@@ -22,6 +22,26 @@ const context = require('./context')
|
|
|
22
22
|
const isStream = require('./is-stream.js')
|
|
23
23
|
const only = require('./only.js')
|
|
24
24
|
|
|
25
|
+
function isHttpErrorLike (err) {
|
|
26
|
+
if (!err || typeof err !== 'object') return false
|
|
27
|
+
if (!(err instanceof Error)) return false
|
|
28
|
+
if (typeof err.expose !== 'boolean') return false
|
|
29
|
+
if (typeof err.statusCode !== 'number') return false
|
|
30
|
+
|
|
31
|
+
const hasMatchingStatusCode = err.status === err.statusCode
|
|
32
|
+
return hasMatchingStatusCode
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
Object.defineProperty(createHttpError.HttpError, Symbol.hasInstance, {
|
|
36
|
+
configurable: true,
|
|
37
|
+
value (err) {
|
|
38
|
+
const matchesPrototype = Function.prototype[Symbol.hasInstance].call(this, err)
|
|
39
|
+
if (matchesPrototype) return true
|
|
40
|
+
if (this !== createHttpError.HttpError) return false
|
|
41
|
+
return isHttpErrorLike(err)
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
25
45
|
/** @typedef {typeof import ('./context') & {
|
|
26
46
|
* app: Application
|
|
27
47
|
* req: import('http').IncomingMessage
|
|
@@ -58,14 +78,14 @@ module.exports = class Application extends Emitter {
|
|
|
58
78
|
/**
|
|
59
79
|
*
|
|
60
80
|
* @param {object} [options] Application options
|
|
61
|
-
* @param {string} [options.env='development'] Environment
|
|
81
|
+
* @param {string} [options.env='development'] Environment. Defaults to `NODE_ENV` or `'development'`.
|
|
62
82
|
* @param {string[]} [options.keys] Signed cookie keys
|
|
63
|
-
* @param {boolean} [options.proxy]
|
|
64
|
-
* @param {number} [options.subdomainOffset] Subdomain offset
|
|
65
|
-
* @param {string} [options.proxyIpHeader] Proxy IP header, defaults to X-Forwarded-For
|
|
66
|
-
* @param {number} [options.maxIpsCount] Max IPs read from proxy IP header,
|
|
83
|
+
* @param {boolean} [options.proxy] When `true`, proxy header fields will be trusted.
|
|
84
|
+
* @param {number} [options.subdomainOffset] Subdomain offset, defaults to `2`
|
|
85
|
+
* @param {string} [options.proxyIpHeader] Proxy IP header, defaults to `X-Forwarded-For`
|
|
86
|
+
* @param {number} [options.maxIpsCount] Max IPs read from proxy IP header, defaults to `0` (means infinity)
|
|
67
87
|
* @param {function} [options.compose] Function to handle middleware composition
|
|
68
|
-
* @param {boolean} [options.asyncLocalStorage]
|
|
88
|
+
* @param {boolean|AsyncLocalStorage} [options.asyncLocalStorage] Pass `true` or an instance of `AsyncLocalStorage` to enable async local storage.
|
|
69
89
|
*
|
|
70
90
|
*/
|
|
71
91
|
|
|
@@ -337,8 +357,9 @@ function respond (ctx) {
|
|
|
337
357
|
}
|
|
338
358
|
|
|
339
359
|
/**
|
|
340
|
-
*
|
|
341
|
-
*
|
|
360
|
+
* Expose HTTP error utilities for consumers.
|
|
361
|
+
* This avoids requiring consumers to depend directly on `http-errors`.
|
|
342
362
|
*/
|
|
343
|
-
|
|
344
|
-
module.exports.HttpError = HttpError
|
|
363
|
+
module.exports.createHttpError = createHttpError
|
|
364
|
+
module.exports.HttpError = createHttpError.HttpError
|
|
365
|
+
module.exports.isHttpError = createHttpError.isHttpError
|
package/lib/context.js
CHANGED
|
@@ -143,7 +143,13 @@ const proto = module.exports = {
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
// then set those specified
|
|
146
|
-
|
|
146
|
+
const headers = err.headers || {}
|
|
147
|
+
Object.keys(headers).forEach(name => {
|
|
148
|
+
const lowerName = name.toLowerCase()
|
|
149
|
+
const generatedHeader = lowerName === 'content-type' || lowerName === 'content-length'
|
|
150
|
+
if (generatedHeader) return
|
|
151
|
+
this.set(name, headers[name])
|
|
152
|
+
})
|
|
147
153
|
|
|
148
154
|
// force text/plain
|
|
149
155
|
this.type = 'text'
|
package/lib/request.js
CHANGED
|
@@ -18,6 +18,8 @@ const only = require('./only.js')
|
|
|
18
18
|
const util = require('util')
|
|
19
19
|
|
|
20
20
|
const IP = Symbol('context#ip')
|
|
21
|
+
const HTTP_URL_PREFIX = /^https?:\/\//i
|
|
22
|
+
const INVALID_IPV6_HOST_CHARACTERS = /[/?#\\\s]/
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
25
|
* Prototype.
|
|
@@ -108,7 +110,7 @@ module.exports = {
|
|
|
108
110
|
|
|
109
111
|
get href () {
|
|
110
112
|
// support: `GET http://example.com/foo`
|
|
111
|
-
if (
|
|
113
|
+
if (HTTP_URL_PREFIX.test(this.originalUrl)) return this.originalUrl
|
|
112
114
|
return this.protocol + '://' + this.host + this.originalUrl
|
|
113
115
|
},
|
|
114
116
|
|
|
@@ -171,7 +173,7 @@ module.exports = {
|
|
|
171
173
|
|
|
172
174
|
get query () {
|
|
173
175
|
const str = this.querystring
|
|
174
|
-
const c = this._querycache = this._querycache ||
|
|
176
|
+
const c = this._querycache = this._querycache || Object.create(null)
|
|
175
177
|
return c[str] || (c[str] = sp.parse(str))
|
|
176
178
|
},
|
|
177
179
|
|
|
@@ -281,8 +283,15 @@ module.exports = {
|
|
|
281
283
|
get hostname () {
|
|
282
284
|
const host = this.host
|
|
283
285
|
if (!host) return ''
|
|
284
|
-
|
|
285
|
-
|
|
286
|
+
const isIPv6 = host[0] === '['
|
|
287
|
+
const hasInvalidIPv6Characters = isIPv6 && INVALID_IPV6_HOST_CHARACTERS.test(host)
|
|
288
|
+
if (hasInvalidIPv6Characters) return ''
|
|
289
|
+
try {
|
|
290
|
+
if (isIPv6) return new URL(`http://${host}`).hostname // IPv6
|
|
291
|
+
return host.split(':', 1)[0]
|
|
292
|
+
} catch {
|
|
293
|
+
return ''
|
|
294
|
+
}
|
|
286
295
|
},
|
|
287
296
|
|
|
288
297
|
/**
|
|
@@ -298,7 +307,9 @@ module.exports = {
|
|
|
298
307
|
if (!this.memoizedURL) {
|
|
299
308
|
const originalUrl = this.originalUrl || '' // avoid undefined in template string
|
|
300
309
|
try {
|
|
301
|
-
this.memoizedURL =
|
|
310
|
+
this.memoizedURL = HTTP_URL_PREFIX.test(originalUrl)
|
|
311
|
+
? new URL(originalUrl)
|
|
312
|
+
: new URL(`${this.protocol}://${this.host}${originalUrl}`)
|
|
302
313
|
} catch (err) {
|
|
303
314
|
this.memoizedURL = Object.create(null)
|
|
304
315
|
}
|
|
@@ -392,7 +403,8 @@ module.exports = {
|
|
|
392
403
|
get length () {
|
|
393
404
|
const len = this.get('Content-Length')
|
|
394
405
|
if (len === '') return
|
|
395
|
-
|
|
406
|
+
const parsed = Number.parseInt(len, 10)
|
|
407
|
+
return Number.isNaN(parsed) ? undefined : parsed
|
|
396
408
|
},
|
|
397
409
|
|
|
398
410
|
/**
|
|
@@ -411,7 +423,7 @@ module.exports = {
|
|
|
411
423
|
if (this.socket.encrypted) return 'https'
|
|
412
424
|
if (!this.app.proxy) return 'http'
|
|
413
425
|
const proto = this.get('X-Forwarded-Proto')
|
|
414
|
-
return proto ? splitCommaSeparatedValues(proto, 1)[0] : 'http'
|
|
426
|
+
return proto ? splitCommaSeparatedValues(proto, 1)[0].toLowerCase() : 'http'
|
|
415
427
|
},
|
|
416
428
|
|
|
417
429
|
/**
|
|
@@ -490,7 +502,9 @@ module.exports = {
|
|
|
490
502
|
get subdomains () {
|
|
491
503
|
const offset = this.app.subdomainOffset
|
|
492
504
|
const hostname = this.hostname
|
|
493
|
-
|
|
505
|
+
const isIPv6 = hostname[0] === '['
|
|
506
|
+
const ip = isIPv6 ? hostname.slice(1, -1) : hostname
|
|
507
|
+
if (net.isIP(ip)) return []
|
|
494
508
|
return hostname
|
|
495
509
|
.split('.')
|
|
496
510
|
.reverse()
|
package/lib/response.js
CHANGED
|
@@ -21,6 +21,19 @@ const getType = require('mime-types').contentType
|
|
|
21
21
|
const isStream = require('./is-stream.js')
|
|
22
22
|
const only = require('./only.js')
|
|
23
23
|
|
|
24
|
+
function isSingleValueHeader (field) {
|
|
25
|
+
const name = field.toLowerCase()
|
|
26
|
+
return name === 'content-type' || name === 'content-length'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeHeader (field, value) {
|
|
30
|
+
const singletonArray = isSingleValueHeader(field) && Array.isArray(value)
|
|
31
|
+
if (!singletonArray) return [field, value]
|
|
32
|
+
|
|
33
|
+
assert(value.length === 1, `${field} must contain exactly one value`)
|
|
34
|
+
return [field, value[0]]
|
|
35
|
+
}
|
|
36
|
+
|
|
24
37
|
/**
|
|
25
38
|
* Prototype.
|
|
26
39
|
*/
|
|
@@ -207,7 +220,7 @@ module.exports = {
|
|
|
207
220
|
|
|
208
221
|
// blob
|
|
209
222
|
if (val instanceof Blob) {
|
|
210
|
-
if (setType) this.type = 'bin'
|
|
223
|
+
if (setType) this.type = val.type || 'bin'
|
|
211
224
|
this.length = val.size
|
|
212
225
|
cleanupPreviousStream()
|
|
213
226
|
return
|
|
@@ -537,11 +550,12 @@ module.exports = {
|
|
|
537
550
|
set (field, val) {
|
|
538
551
|
if (this.headerSent || !field) return
|
|
539
552
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
553
|
+
const entries = typeof field === 'string' ? [[field, val]] : Object.entries(field)
|
|
554
|
+
const headers = entries.map(([name, value]) => normalizeHeader(name, value))
|
|
555
|
+
|
|
556
|
+
headers.forEach(([name, value]) => {
|
|
557
|
+
this.res.setHeader(name, value)
|
|
558
|
+
})
|
|
545
559
|
},
|
|
546
560
|
|
|
547
561
|
/**
|
package/lib/search-params.js
CHANGED
|
@@ -4,7 +4,7 @@ module.exports = {
|
|
|
4
4
|
stringify: (obj) => {
|
|
5
5
|
const searchParams = new URLSearchParams()
|
|
6
6
|
const addKey = (k, v, params) => {
|
|
7
|
-
const val = typeof v === 'string' || typeof v === 'number' ? v : ''
|
|
7
|
+
const val = typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' ? v : ''
|
|
8
8
|
params.append(k, val)
|
|
9
9
|
}
|
|
10
10
|
|
|
@@ -26,7 +26,13 @@ module.exports = {
|
|
|
26
26
|
const obj = {}
|
|
27
27
|
for (const key of searchParams.keys()) {
|
|
28
28
|
const values = searchParams.getAll(key)
|
|
29
|
-
|
|
29
|
+
const value = values.length <= 1 ? values[0] : values
|
|
30
|
+
Object.defineProperty(obj, key, {
|
|
31
|
+
value,
|
|
32
|
+
writable: true,
|
|
33
|
+
enumerable: true,
|
|
34
|
+
configurable: true
|
|
35
|
+
})
|
|
30
36
|
}
|
|
31
37
|
return obj
|
|
32
38
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koa",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "Koa web app framework",
|
|
5
5
|
"main": "lib/application.js",
|
|
6
6
|
"exports": {
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"vary": "^1.1.2"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"c8": "^
|
|
59
|
+
"c8": "^12.0.0",
|
|
60
60
|
"gen-esm-wrapper": "^1.1.3",
|
|
61
61
|
"snazzy": "^9.0.0",
|
|
62
62
|
"standard": "^17.1.2",
|