koa 3.2.1 → 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 +20 -7
- 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
|
}
|
|
@@ -412,7 +423,7 @@ module.exports = {
|
|
|
412
423
|
if (this.socket.encrypted) return 'https'
|
|
413
424
|
if (!this.app.proxy) return 'http'
|
|
414
425
|
const proto = this.get('X-Forwarded-Proto')
|
|
415
|
-
return proto ? splitCommaSeparatedValues(proto, 1)[0] : 'http'
|
|
426
|
+
return proto ? splitCommaSeparatedValues(proto, 1)[0].toLowerCase() : 'http'
|
|
416
427
|
},
|
|
417
428
|
|
|
418
429
|
/**
|
|
@@ -491,7 +502,9 @@ module.exports = {
|
|
|
491
502
|
get subdomains () {
|
|
492
503
|
const offset = this.app.subdomainOffset
|
|
493
504
|
const hostname = this.hostname
|
|
494
|
-
|
|
505
|
+
const isIPv6 = hostname[0] === '['
|
|
506
|
+
const ip = isIPv6 ? hostname.slice(1, -1) : hostname
|
|
507
|
+
if (net.isIP(ip)) return []
|
|
495
508
|
return hostname
|
|
496
509
|
.split('.')
|
|
497
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",
|