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 CHANGED
@@ -2,3 +2,5 @@ import mod from "../lib/application.js";
2
2
 
3
3
  export default mod;
4
4
  export const HttpError = mod.HttpError;
5
+ export const createHttpError = mod.createHttpError;
6
+ export const isHttpError = mod.isHttpError;
@@ -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 { HttpError } = require('http-errors')
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] Trust proxy headers
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, default to 0 (means infinity)
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] Enable AsyncLocalStorage, default to false
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
- * Make HttpError available to consumers of the library so that consumers don't
341
- * have a direct dependency upon `http-errors`
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
- this.set(err.headers)
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 (/^https?:\/\//i.test(this.originalUrl)) return this.originalUrl
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
- if (host[0] === '[') return this.URL.hostname || '' // IPv6
285
- return host.split(':', 1)[0]
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 = new URL(`${this.protocol}://${this.host}${originalUrl}`)
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
- return ~~len
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
- if (net.isIP(hostname)) return []
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
- if (typeof field === 'string') {
541
- this.res.setHeader(field, val)
542
- } else {
543
- Object.keys(field).forEach(header => this.res.setHeader(header, field[header]))
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
  /**
@@ -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
- obj[key] = values.length <= 1 ? values[0] : values
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.2.0",
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": "^10.1.3",
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",