Compare commits

...

40 Commits

Author SHA1 Message Date
Jonatan Nilsson dc572cd83a Fix slight bug in context error handler 2021-01-04 17:33:15 +00:00
Jonatan Nilsson d1bcd10f82 Update version 2021-01-04 14:07:36 +00:00
Jonatan Nilsson 8fcdbb003c remove more dependencies 2021-01-04 14:06:20 +00:00
Jonatan Nilsson cd811a7046 2.9.0 2019-12-18 21:33:12 +00:00
Jonatan Nilsson 73d057a762 package: Use debug-ms dependancy 2019-12-18 21:32:45 +00:00
Jonatan Nilsson bbc03cbacd 2.8.9 2019-12-18 21:29:50 +00:00
Jonatan Nilsson 969ffa59f3 minetype: Add sag support 2019-12-18 21:29:49 +00:00
Jonatan Nilsson ea8acbef1a 2.8.8 2019-12-18 21:29:49 +00:00
Jonatan Nilsson 453b81ab60
Update Readme.md 2019-10-12 14:44:03 +00:00
Jonatan Nilsson 5a9a3f0dc2 Remove is-type and replace with a helper function :) 2019-10-12 14:31:02 +00:00
Jonatan Nilsson 54aa155328 2.8.7 2019-10-12 05:01:50 +00:00
Jonatan Nilsson e3b8fd788f Update history/readme 2019-10-12 05:01:35 +00:00
Jonatan Nilsson 141d91b216 Add back support for accepts without using npm accepts 2019-10-12 04:30:06 +00:00
Jonatan Nilsson 4078dc1182 2.8.6 2019-10-12 01:43:27 +00:00
Jonatan Nilsson eca102bbd3 Increment history due to forgetting to update readme during last version publish 2019-10-12 01:43:23 +00:00
Jonatan Nilsson 1f9d2eb86d History: Update to 2.8.5 2019-10-12 01:41:54 +00:00
Jonatan Nilsson 056a0e7d76 2.8.5 2019-10-12 01:41:54 +00:00
Jonatan Nilsson 450c835837 package: Update repository to link here 2019-10-12 01:41:54 +00:00
Jonatan Nilsson ca6091af55
Update Readme.md 2019-10-12 01:39:47 +00:00
Jonatan Nilsson 1edc7f34a8
Update Readme.md 2019-10-12 01:37:52 +00:00
Jonatan Nilsson 05b3cbf7d6 2.8.4 2019-10-12 01:36:26 +00:00
Jonatan Nilsson fb65d1c7ce
Update History.md 2019-10-12 01:35:28 +00:00
Jonatan Nilsson 8235c2491c
Update Readme.md 2019-10-12 01:23:25 +00:00
Jonatan Nilsson 7e7dc99e49 2.8.3 2019-10-12 00:57:49 +00:00
Jonatan Nilsson 8157e9e752 Remove koa-compose as as it can just be included in koa 2019-10-12 00:55:24 +00:00
Jonatan Nilsson d655f208cb Remove destroy, encoder, error-inject, escape-html, koa-is-json, on-finished, type-is and vary 2019-10-12 00:17:22 +00:00
Jonatan Nilsson 6b27b844ff Add fast parse and remove parseurl 2019-10-11 19:41:53 +00:00
Jonatan Nilsson bd05c21456 Fix linter errors 2019-10-11 17:46:53 +00:00
Jonatan Nilsson 46b43d831b update description and such 2019-10-11 17:34:28 +00:00
Jonatan Nilsson fad4d10057 Remove http-assert and replace it with just a simple function :) 2019-10-11 16:57:01 +00:00
Jonatan Nilsson dd35564df4 Remove statuses and replace http-errors with http-errors-lite 2019-10-11 16:40:06 +00:00
Jonatan Nilsson 28fee01c3f Remove delegates, dead, is-generator-function, kia-convert and only 2019-10-09 00:32:09 +00:00
Jonatan Nilsson c9459b19ba Remove cookies, replace debug with debug-ms 2019-10-08 18:44:33 +00:00
Jonatan Nilsson 2ef7846b5f Remove accepts, cache-content-type and content-type.
Replace content-disposition with one that doesn’t use safe-buffer
2019-09-29 19:17:14 +00:00
fengmk2 a0d2816cba
Release 2.8.2 2019-09-28 12:51:10 +08:00
fengmk2 54e8fab3e3
fix: encode redirect url if not already encoded (#1384)
Same bug from express 76eaa326ee
2019-09-28 12:49:57 +08:00
Robert Nagy 817b498305 test: fix body test (#1375)
Setting body will set the content-length header. Unless the
corresponding number of bytes are sent the response will
be aborted and not emit 'end'.
2019-08-23 18:57:56 +08:00
Robert Nagy f75d445535 test: fix end after end (#1374) 2019-08-23 18:57:32 +08:00
dead-horse 061c21f336 Release 2.8.1 2019-08-19 12:36:21 +08:00
dead-horse 287e589ac7 fix: make options more compatibility 2019-08-19 11:54:41 +08:00
38 changed files with 3221 additions and 1375 deletions

View File

@ -1 +1,3 @@
extends: koa
rules:
operator-linebreak: [error, before]

View File

@ -1,546 +1,41 @@
2.8.0 / 2019-08-19
==================
**features**
* [[`5afff89`](http://github.com/koajs/koa/commit/5afff89eca0efe7081309dc2d123309e825df221)] - feat: accept options in the Application constructor (#1372) (Jake <<djakelambert@gmail.com>>)
**fixes**
* [[`ff70bdc`](http://github.com/koajs/koa/commit/ff70bdc75a30a37f63fc1f7d8cbae3204df3d982)] - fix: typo on document (#1355) (Jeff <<jeff.tian@outlook.com>>)
**others**
* [[`3b23865`](http://github.com/koajs/koa/commit/3b23865340cfba075f61f7dba0ea31fcc27260ec)] - docs: parameter of request.get is case-insensitive (#1373) (Gunnlaugur Thor Briem <<gunnlaugur@gmail.com>>)
* [[`a245d18`](http://github.com/koajs/koa/commit/a245d18a131341feec4f87659746954e78cae780)] - docs: Update response.socket (#1357) (Jeff <<jeff.tian@outlook.com>>)
* [[`d1d65dd`](http://github.com/koajs/koa/commit/d1d65dd29d7bbaf9ea42eaa5fcb0da3fb4df98e9)] - chore(deps): install egg-bin, mm as devDeps not deps (#1366) (Edvard Chen <<pigeon73101@gmail.com>>)
* [[`2c86b10`](http://github.com/koajs/koa/commit/2c86b10feafd868ebd071dda3a222e6f51972b5d)] - test: remove jest and use egg-bin(mocha) (#1363) (Yiyu He <<dead_horse@qq.com>>)
* [[`219bf22`](http://github.com/koajs/koa/commit/219bf22237b11bc375e2e110b93db512f1acfdd4)] - docs(context): update link (#1354) (Peng Jie <<bivinity.pengzjie@gmail.com>>)
* [[`52a6737`](http://github.com/koajs/koa/commit/52a673703a87a93c0f6a8552e6bd73caba66d2eb)] - chore: ignore Intellij IDEA project files (#1361) (Imon-Haque <<38266345+Imon-Haque@users.noreply.github.com>>)
* [[`b9e3546`](http://github.com/koajs/koa/commit/b9e35469d3bbd0a1ee92e0a815ce2512904d4a18)] - docs(api): fix keygrip link (#1350) (Peng Jie <<bivinity.pengzjie@gmail.com>>)
* [[`d4bdb5e`](http://github.com/koajs/koa/commit/d4bdb5ed9e2fe06ec44698b66c029f624135a0ab)] - chore: update eslint and fix lint errors (dead-horse <<dead_horse@qq.com>>)
* [[`12960c4`](http://github.com/koajs/koa/commit/12960c437cc25c53e682cfe5bff06d74a5bb1eb9)] - build: test on 8/10/12 (dead-horse <<dead_horse@qq.com>>)
* [[`00e8f7a`](http://github.com/koajs/koa/commit/00e8f7a1b7603aabdb7fb3567f485cb1c2076702)] - docs: ctx.type aliases ctx.response, not ctx.request (#1343) (Alex Berk <<berkalexanderc@gmail.com>>)
* [[`62f29eb`](http://github.com/koajs/koa/commit/62f29eb0c4dee01170a5511615e5bcc9faca26ca)] - docs(context): update cookies link (#1348) (Peng Jie <<dean.leehom@gmail.com>>)
* [[`b7fc526`](http://github.com/koajs/koa/commit/b7fc526ea49894f366153bd32997e02568c0b8a6)] - docs: fix typo in cookie path default value docs (#1340) (Igor Adamenko <<igoradamenko@users.noreply.github.com>>)
* [[`23f7f54`](http://github.com/koajs/koa/commit/23f7f545abfe1fb6499cd61cc8ff41fd86cef4a0)] - chore: simplify variable (#1332) (kzhang <<godky@users.noreply.github.com>>)
* [[`132c9ee`](http://github.com/koajs/koa/commit/132c9ee63f92a586a120ed3bd6b7ef023badb8bb)] - docs: Clarify the format of request.headers (#1325) (Dobes Vandermeer <<dobesv@gmail.com>>)
* [[`5810f27`](http://github.com/koajs/koa/commit/5810f279a4caeda115f39e429c9671795613abf8)] - docs: Removed Document in Progress note in Koa vs Express (#1336) (Andrew Peterson <<andrew@andpeterson.com>>)
* [[`75233d9`](http://github.com/koajs/koa/commit/75233d974a30af6e3b8ab38a73e5ede67172fc1c)] - chore: Consider removing this return statement; it will be ignored. (#1322) (Vern Brandl <<tkvern@users.noreply.github.com>>)
* [[`04e07fd`](http://github.com/koajs/koa/commit/04e07fdc620841068f12b8edf36f27e6592a0a18)] - test: Buffer() is deprecated due to security and usability issues. so use the Buffer.alloc() instead (#1321) (Vern Brandl <<tkvern@users.noreply.github.com>>)
* [[`130e363`](http://github.com/koajs/koa/commit/130e363856747b487652f04b5550056d7778e43a)] - docs: use 'fs-extra' instead of 'fs-promise' (#1309) (rosald <<35028438+rosald@users.noreply.github.com>>)
* [[`2f2078b`](http://github.com/koajs/koa/commit/2f2078bf998bd3f44289ebd17eeccf5e12e4c134)] - chore: Update PR-welcome badge url (#1299) (James George <<jamesgeorge998001@gmail.com>>)
2.7.0 / 2019-01-28
==================
**features**
* [[`b7bfa71`](http://github.com/koajs/koa/commit/b7bfa7113b8d1af49a57ab767f24a599ed92044f)] - feat: change set status assert, allowing valid custom statuses (#1308) (Martin Iwanowski <<martin@iwanowski.se>>)
**others**
* [[`72f325b`](http://github.com/koajs/koa/commit/72f325b78edd0dc2aac940a76ce5f644005ce4c3)] - chore: add pr welcoming badge (#1291) (James George <<jamesgeorge998001@gmail.com>>)
* [[`b15115b`](http://github.com/koajs/koa/commit/b15115b2cbfffe15827cd5e4368267d417b72f08)] - chore: Reduce unnecessary variable declarations (#1298) (call me saisai <<1457358080@qq.com>>)
* [[`ad91ce2`](http://github.com/koajs/koa/commit/ad91ce2346cb34e5d5a49d07dd952d15f6c832a3)] - chore: license 2019 (dead-horse <<dead_horse@qq.com>>)
* [[`b25e79d`](http://github.com/koajs/koa/commit/b25e79dfb599777a38157bd419395bd28369ee86)] - Mark two examples as live for the corresponding documentation change in https://github.com/koajs/koajs.com/pull/38. (#1031) (Francisco Ryan Tolmasky I <<tolmasky@gmail.com>>)
* [[`d9ef603`](http://github.com/koajs/koa/commit/d9ef60398e88f2c2f958ab2b159d38052ffe7f8a)] - chore: Optimize array split (#1295) (Mikhail Bodrov <<connormiha1@gmail.com>>)
* [[`9be8583`](http://github.com/koajs/koa/commit/9be858312553002841725b617050aaff3c48951d)] - chore: replace ~~ with Math.trunc in res.length (option) (#1288) (jeremiG <<gendronjeremi@gmail.com>>)
* [[`7e46c20`](http://github.com/koajs/koa/commit/7e46c2058cb5994809eab5f4dbb12f21e937c72b)] - docs: add link to the license file (#1290) (James George <<jamesgeorge998001@gmail.com>>)
* [[`48993ad`](http://github.com/koajs/koa/commit/48993ade9b0831fbce28d94b3b0963a4b0dccbdd)] - docs: Document other body types (#1285) (Douglas Wade <<douglas.b.wade@gmail.com>>)
* [[`acb388b`](http://github.com/koajs/koa/commit/acb388bc0546b48fca11dce8aa7a595af2cda5e2)] - docs: Add security vulnerability disclosure instructions to the Readme (#1283) (Douglas Wade <<douglas.b.wade@gmail.com>>)
* [[`a007198`](http://github.com/koajs/koa/commit/a007198fa23c19902b1f3ffb81498629e0e9c875)] - docs: Document ctx.app.emit (#1284) (Douglas Wade <<douglas.b.wade@gmail.com>>)
* [[`f90e825`](http://github.com/koajs/koa/commit/f90e825da9d505c11b4262c50cd54553f979c300)] - docs: response.set(fields) won't overwrites previous header fields(#1282) (Douglas Wade <<douglas.b.wade@gmail.com>>)
* [[`fc93c05`](http://github.com/koajs/koa/commit/fc93c05f68398f30abc46fd16ae6c673a1eee099)] - docs: update readme to add babel 7 instructions (#1274) (Vikram Rangaraj <<vik120@icloud.com>>)
* [[`5560f72`](http://github.com/koajs/koa/commit/5560f729124f022ffed00085aafea43dded7fb03)] - chore: use the ability of `content-type` lib directly (#1276) (Jordan <<mingmingwon@gmail.com>>)
2.6.2 / 2018-11-10
==================
**fixes**
* [[`9905199`](http://github.com/koajs/koa/commit/99051992a9f45eb0dd79e062681d6f5d366deb41)] - fix: Status message is not supported on HTTP/2 (#1264) (André Cruz <<andre@cabine.org>>)
**others**
* [[`325792a`](http://github.com/koajs/koa/commit/325792aee92de0ba6fea306657933fc63dc00474)] - docs: add table of contents for guide.md (#1267) (ZYSzys <<zyszys98@gmail.com>>)
* [[`71aaa29`](http://github.com/koajs/koa/commit/71aaa29591d6681f8579486f18d32ba1ee651a5b)] - docs: fix spelling in throw docs (#1269) (Martin Iwanowski <<martin@iwanowski.se>>)
* [[`bc81ca9`](http://github.com/koajs/koa/commit/bc81ca9414296234c764b7306a19ba72b2e59b52)] - chore: use res instead of this.res (#1271) (Jordan <<mingmingwon@gmail.com>>)
* [[`0251b38`](http://github.com/koajs/koa/commit/0251b38a8405471892c5eeaba7c8d54bd7028214)] - test: node v11 on travis (#1265) (Martin Iwanowski <<martin@iwanowski.se>>)
* [[`88b92b4`](http://github.com/koajs/koa/commit/88b92b43153f21609aee71d47abcd4dc27a6586d)] - doc: updated docs for throw() to pass status as first param. (#1268) (Waleed Ashraf <<waleedashraf@outlook.com>>)
2.6.1 / 2018-10-23
==================
**fixes**
* [[`4964242`](http://github.com/koajs/koa/commit/49642428342e5f291eb9d690802e83ed830623b5)] - fix: use X-Forwarded-Host first on app.proxy present (#1263) (fengmk2 <<fengmk2@gmail.com>>)
2.6.0 / 2018-10-23
==================
**features**
* [[`9c5c58b`](http://github.com/koajs/koa/commit/9c5c58b18363494976185e7ddc790ac63de840ed)] - feat: use :authority header of http2 requests as host (#1262) (Martin Michaelis <<code@mgjm.de>>)
* [[`9146024`](http://github.com/koajs/koa/commit/9146024e1094e8bb871ab15d1b7fc556a710732f)] - feat: response.attachment append a parameter: options from contentDisposition (#1240) (小雷 <<863837949@qq.com>>)
**others**
* [[`d32623b`](http://github.com/koajs/koa/commit/d32623baa7a6273d47be67d587ad4ea0ecffc5de)] - docs: Update error-handling.md (#1239) (urugator <<j.placek@centrum.cz>>)
2.5.3 / 2018-09-11
==================
**fixes**
* [[`2ee32f5`](http://github.com/koajs/koa/commit/2ee32f50b88b383317e33cc0a4bfaa5f2eadead7)] - fix: pin debug@~3.1.0 avoid deprecated warnning (#1245) (fengmk2 <<fengmk2@gmail.com>>)
**others**
* [[`2180839`](http://github.com/koajs/koa/commit/2180839eda2cb16edcfda46ccfe24711680af850)] - docs: Update koa-vs-express.md (#1230) (Clayton Ray <<iamclaytonray@gmail.com>>)
2.5.2 / 2018-07-12
==================
* deps: upgrade all dependencies
* perf: avoid stringify when set header (#1220)
* perf: cache content type's result (#1218)
* perf: lazy init cookies and ip when first time use it (#1216)
* chore: fix comment & approve cov (#1214)
* docs: fix grammar
* test&cov: add test case (#1211)
* Lazily initialize `request.accept` and delegate `context.accept` (#1209)
* fix: use non deprecated custom inspect (#1198)
* Simplify processes in the getter `request.protocol` (#1203)
* docs: better demonstrate middleware flow (#1195)
* fix: Throw a TypeError instead of a AssertionError (#1199)
* chore: mistake in a comment (#1201)
* chore: use this.res.socket insteadof this.ctx.req.socket (#1177)
* chore: Using "listenerCount" instead of "listeners" (#1184)
2.5.1 / 2018-04-27
==================
* test: node v10 on travis (#1182)
* fix tests: remove unnecessary assert doesNotThrow and api calls (#1170)
* use this.response insteadof this.ctx.response (#1163)
* deps: remove istanbul (#1151)
* Update guide.md (#1150)
2.5.0 / 2018-02-11
==================
* feat: ignore set header/status when header sent (#1137)
* run coverage using --runInBand (#1141)
* [Update] license year to 2018 (#1130)
* docs: small grammatical fix in api docs index (#1111)
* docs: fixed typo (#1112)
* docs: capitalize K in word koa (#1126)
* Error handling: on non-error throw try to stringify if error is an object (#1113)
* Use eslint-config-koa (#1105)
* Update mgol's name in AUTHORS, add .mailmap (#1100)
* Avoid generating package locks instead of ignoring them (#1108)
* chore: update copyright year to 2017 (#1095)
2.4.1 / 2017-11-06
==================
* fix bad merge w/ 2.4.0
2.4.0 / 2017-11-06
==================
UNPUBLISHED
* update `package.engines.node` to be more strict
* update `fresh@^0.5.2`
* fix: `inspect()` no longer crashes `context`
* fix: gated `res.statusMessage` for HTTP/2
* added: `app.handleRequest()` is exposed
2.3.0 / 2017-06-20
==================
* fix: use `Buffer.from()`
* test on node 7 & 8
* add `package-lock.json` to `.gitignore`
* run `lint --fix`
* add `request.header` in addition to `request.headers`
* add IPv6 hostname support
2.2.0 / 2017-03-14
==================
* fix: drop `package.engines.node` requirement to >= 6.0.0
* this fixes `yarn`, which errors when this semver range is not satisfied
* bump `cookies@~0.7.0`
* bump `fresh@^0.5.0`
2.1.0 / 2017-03-07
==================
* added: return middleware chain promise from `callback()` #848
* added: node v7.7+ `res.getHeaderNames()` support #930
* added: `err.headerSent` in error handling #919
* added: lots of docs!
2.0.1 / 2017-02-25
==================
NOTE: we hit a versioning snafu. `v2.0.0` was previously released,
so `v2.0.1` is released as the first `v2.x` with a `latest` tag.
* upgrade mocha #900
* add names to `application`'s request and response handlers #805
* breaking: remove unused `app.name` #899
* breaking: drop official support for node < 7.6
2.0.0 / ??????????
==================
* Fix malformed content-type header causing exception on charset get (#898)
* fix: subdomains should be [] if the host is an ip (#808)
* don't pre-bound onerror [breaking change] (#800)
* fix `ctx.flushHeaders()` to use `res.flushHeaders()` instead of `res.writeHead()` (#795)
* fix(response): correct response.writable logic (#782)
* merge v1.1.2 and v1.2.0 changes
* include `koa-convert` so that generator functions still work
* NOTE: generator functions are deprecated in v2 and will be removed in v3
* improve linting
* improve docs
2.0.0-alpha.8 / 2017-02-13
==================
* Fix malformed content-type header causing exception on charset get (#898)
2.0.0-alpha.7 / 2016-09-07
==================
* fix: subdomains should be [] if the host is an ip (#808)
2.0.0-alpha.6 / 2016-08-29
==================
* don't pre-bound onerror [breaking change]
2.0.0-alpha.5 / 2016-08-10
==================
* fix `ctx.flushHeaders()` to use `res.flushHeaders()` instead of `res.writeHead()`
2.0.0-alpha.4 / 2016-07-23
==================
* fix `response.writeable` during pipelined requests
1.2.0 / 2016-03-03
==================
* add support for `err.headers` in `ctx.onerror()`
- see: https://github.com/koajs/koa/pull/668
- note: you should set these headers in your custom error handlers as well
- docs: https://github.com/koajs/koa/blob/master/docs/error-handling.md
* fix `cookies`' detection of http/https
- see: https://github.com/koajs/koa/pull/614
* deprecate `app.experimental = true`. Koa v2 does not use this signature.
* add a code of conduct
* test against the latest version of node
* add a lot of docs
1.1.2 / 2015-11-05
==================
* ensure parseurl always working as expected
* fix Application.inspect() missing .proxy value.
2.0.0-alpha.3 / 2015-11-05
==================
* ensure parseurl always working as expected. #586
* fix Application.inspect() missing .proxy value. Closes #563
2.0.0-alpha.2 / 2015-10-27
==================
* remove `co` and generator support completely
* improved documentation
* more refactoring into ES6
2.0.0-alpha.1 / 2015-10-22
==================
* change the middleware signature to `async (ctx, next) => await next()`
* drop node < 4 support and rewrite the codebase in ES6
1.1.1 / 2015-10-22
==================
* do not send a content-type when the type is unknown #536
1.1.0 / 2015-10-11
==================
* add `app.silent=<Boolean>` to toggle error logging @tejasmanohar #486
* add `ctx.origin` @chentsulin #480
* various refactoring
- add `use strict` everywhere
1.0.0 / 2015-08-22
==================
* add `this.req` check for `querystring()`
* don't log errors with `err.expose`
* `koa` now follows semver!
0.21.0 / 2015-05-23
==================
* empty `request.query` objects are now always the same instance
* bump `fresh@0.3.0`
0.20.0 / 2015-04-30
==================
Breaking change if you're using `this.get('ua') === undefined` etc.
For more details please checkout [#438](https://github.com/koajs/koa/pull/438).
* make sure helpers return strict string
* feat: alias response.headers to response.header
0.19.1 / 2015-04-14
==================
* non-error thrown, fixed #432
0.19.0 / 2015-04-05
==================
* `req.host` and `req.hostname` now always return a string (semi-breaking change)
* improved test coverage
0.18.1 / 2015-03-01
==================
* move babel to `devDependencies`
0.18.0 / 2015-02-14
==================
* experimental es7 async function support via `app.experimental = true`
* use `content-type` instead of `media-typer`
0.17.0 / 2015-02-05
==================
Breaking change if you're using an old version of node v0.11!
Otherwise, you should have no trouble upgrading.
* official iojs support
* drop support for node.js `>= 0.11.0 < 0.11.16`
* use `Object.setPrototypeOf()` instead of `__proto__`
* update dependencies
0.16.0 / 2015-01-27
==================
* add `res.append()`
* fix path usage for node@0.11.15
0.15.0 / 2015-01-18
==================
* add `this.href`
0.14.0 / 2014-12-15
==================
* remove `x-powered-by` response header
* fix the content type on plain-text redirects
* add ctx.state
* bump `co@4`
* bump dependencies
0.13.0 / 2014-10-17
==================
* add this.message
* custom status support via `statuses`
0.12.2 / 2014-09-28
==================
* use wider semver ranges for dependencies koa maintainers also maintain
0.12.1 / 2014-09-21
==================
* bump content-disposition
* bump statuses
0.12.0 / 2014-09-20
==================
* add this.assert()
* use content-disposition
0.11.0 / 2014-09-08
==================
* fix app.use() assertion #337
* bump a lot of dependencies
0.10.0 / 2014-08-12
==================
* add `ctx.throw(err, object)` support
* add `ctx.throw(err, status, object)` support
0.9.0 / 2014-08-07
==================
* add: do not set `err.expose` to true when err.status not a valid http status code
* add: alias `request.headers` as `request.header`
* add context.inspect(), cleanup app.inspect()
* update cookies
* fix `err.status` invalid lead to uncaughtException
* fix middleware gif, close #322
0.8.2 / 2014-07-27
==================
* bump co
* bump parseurl
0.8.1 / 2014-06-24
==================
* bump type-is
0.8.0 / 2014-06-13
==================
* add `this.response.is()``
* remove `.status=string` and `res.statusString` #298
0.7.0 / 2014-06-07
==================
* add `this.lastModified` and `this.etag` as both getters and setters for ubiquity #292.
See koajs/koa@4065bf7 for an explanation.
* refactor `this.response.vary()` to use [vary](https://github.com/expressjs/vary) #291
* remove `this.response.append()` #291
0.6.3 / 2014-06-06
==================
* fix res.type= when the extension is unknown
* assert when non-error is passed to app.onerror #287
* bump finished
0.6.2 / 2014-06-03
==================
* switch from set-type to mime-types
0.6.1 / 2014-05-11
==================
* bump type-is
* bump koa-compose
0.6.0 / 2014-05-01
==================
* add nicer error formatting
* add: assert object type in ctx.onerror
* change .status default to 404. Closes #263
* remove .outputErrors, suppress output when handled by the dev. Closes #272
* fix content-length when body is re-assigned. Closes #267
0.5.5 / 2014-04-14
==================
* fix length when .body is missing
* fix: make sure all intermediate stream bodies will be destroyed
0.5.4 / 2014-04-12
==================
* fix header stripping in a few cases
0.5.3 / 2014-04-09
==================
* change res.type= to always default charset. Closes #252
* remove ctx.inspect() implementation. Closes #164
0.5.2 / 2014-03-23
==================
* fix: inspection of `app` and `app.toJSON()`
* fix: let `this.throw`n errors provide their own status
* fix: overwriting of `content-type` w/ `HEAD` requests
* refactor: use statuses
* refactor: use escape-html
* bump dev deps
0.5.1 / 2014-03-06
==================
* add request.hostname(getter). Closes #224
* remove response.charset and ctx.charset (too confusing in relation to ctx.type) [breaking change]
* fix a debug() name
0.5.0 / 2014-02-19
==================
* add context.charset
* add context.charset=
* add request.charset
* add response.charset
* add response.charset=
* fix response.body= html content sniffing
* change ctx.length and ctx.type to always delegate to response object [breaking change]
0.4.0 / 2014-02-11
==================
* remove app.jsonSpaces settings - moved to [koa-json](https://github.com/koajs/json)
* add this.response=false to bypass koa's response handling
* fix response handling after body has been sent
* changed ctx.throw() to no longer .expose 5xx errors
* remove app.keys getter/setter, update cookies, and remove keygrip deps
* update fresh
* update koa-compose
0.3.0 / 2014-01-17
==================
* add ctx.host= delegate
* add req.host=
* add: context.throw supports Error instances
* update co
* update cookies
0.2.1 / 2013-12-30
==================
* add better 404 handling
* add check for fn._name in debug() output
* add explicit .toJSON() calls to ctx.toJSON()
0.2.0 / 2013-12-28
==================
* add support for .throw(status, msg). Closes #130
* add GeneratorFunction assertion for app.use(). Closes #120
* refactor: move `.is()` to `type-is`
* refactor: move content negotiation to "accepts"
* refactor: allow any streams with .pipe method
* remove `next` in callback for now
0.1.2 / 2013-12-21
==================
* update co, koa-compose, keygrip
* use on-socket-error
* add throw(status, msg) support
* assert middleware is GeneratorFunction
* ducktype stream checks
* remove `next` is `app.callback()`
0.1.1 / 2013-12-19
==================
* fix: cleanup socker error handler on response
2.8.7 / 2019-10-11
==================
Re-implemented accepts, acceptsEncoding, acceptsLanguage and etc.
2.8.6 / 2019-10-11
==================
Unofficial fork of [Koa 2.8.2](https://github.com/koajs/koa) that is much smaller and lighter on dependancies:
Removed:
* accepts
* cache-content-type
* content-type
* cookies
* delegates
* depd
* destroy
* encodeurl
* error-inject
* escape-html
* http-assert
* is-generator-function
* koa-compose
* koa-convert
* koa-is-json
* on-finished
* only
* parseurl
* statuses
* vary
Replaced:
* http-errors -> http-errors-lite
* debug -> debug-ms (includes ms so one less dependancy)
* content-disposition -> [content-disposition](https://github.com/jharrilim/content-disposition/commit/572383f01c83ea237beb46a307eb6748394f4f92)
Older history
=============
See here: [Koa History.md](https://github.com/koajs/koa/blob/master/History.md)

165
Readme.md
View File

@ -1,13 +1,20 @@
## Koa-lite
This is a tiny (improved) fork of [Koa](https://github.com/koajs/koa) that is almost 80% smaller in disc space and uses a lot less dependancies, a total of 4 packages compared to Koa's 42 package installation. It also uses some improved versions to some of the previous dependencies. Overall the whole installation can be expected to go from 807KB to around 160KB.
### Breaking change:
One of the core mechanic was removed from the core as it should be found in a middleware instead (in my opinion) and that would be ctx.cookies() as I never found myself using cookies ever in all my projects.
<img src="/docs/logo.png" alt="Koa middleware framework for nodejs"/>
[![gitter][gitter-image]][gitter-url]
[![NPM version][npm-image]][npm-url]
[![build status][travis-image]][travis-url]
[![Test coverage][coveralls-image]][coveralls-url]
[![OpenCollective Backers][backers-image]](#backers)
[![OpenCollective Sponsors][sponsors-image]](#sponsors)
[![PR's Welcome][pr-welcoming-image]][pr-welcoming-url]
## What is Koa
Expressive HTTP middleware framework for node.js to make web applications and APIs more enjoyable to write. Koa's middleware stack flows in a stack-like manner, allowing you to perform actions downstream then filter and manipulate the response upstream.
Only methods that are common to nearly all HTTP servers are integrated directly into Koa's small ~570 SLOC codebase. This
@ -20,13 +27,13 @@
Koa requires __node v7.6.0__ or higher for ES2015 and async function support.
```
$ npm install koa
$ npm install koa-lite
```
## Hello Koa
```js
const Koa = require('koa');
const Koa = require('koa-lite');
const app = new Koa();
// response
@ -79,15 +86,6 @@ app.use((ctx, next) => {
});
```
### Koa v1.x Middleware Signature
The middleware signature changed between v1.x and v2.x. The older signature is deprecated.
**Old signature middleware support will be removed in v3**
Please see the [Migration Guide](docs/migration.md) for more information on upgrading from v1.x and
using v1.x middleware with v2.x.
## Context, Request and Response
Each middleware receives a Koa `Context` object that encapsulates an incoming
@ -103,17 +101,6 @@ Koa's `Request` object provides helpful methods for working with
http requests which delegate to an [IncomingMessage](https://nodejs.org/api/http.html#http_class_http_incomingmessage)
from the node `http` module.
Here is an example of checking that a requesting client supports xml.
```js
app.use(async (ctx, next) => {
ctx.assert(ctx.request.accepts('xml'), 406);
// equivalent to:
// if (!ctx.request.accepts('xml')) ctx.throw(406);
await next();
});
```
Koa provides a `Response` object as the `response` property of the `Context`.
Koa's `Response` object provides helpful methods for working with
http responses which delegate to a [ServerResponse](https://nodejs.org/api/http.html#http_class_http_serverresponse)
@ -136,8 +123,7 @@ app.use(async (ctx, next) => {
```
The `Context` object also provides shortcuts for methods on its `request` and `response`. In the prior
examples, `ctx.type` can be used instead of `ctx.response.type` and `ctx.accepts` can be used
instead of `ctx.request.accepts`.
examples, `ctx.type` can be used instead of `ctx.response.type`.
For more information on `Request`, `Response` and `Context`, see the [Request API Reference](docs/api/request.md),
[Response API Reference](docs/api/response.md) and [Context API Reference](docs/api/context.md).
@ -160,40 +146,6 @@ Learn more about the application object in the [Application API Reference](docs/
- [FAQ](docs/faq.md)
- [API documentation](docs/api/index.md)
## Babel setup
If you're not using `node v7.6+`, we recommend setting up `babel` with [`@babel/preset-env`](https://babeljs.io/docs/en/next/babel-preset-env):
```bash
$ npm install @babel/register @babel/preset-env @babel/cli --save-dev
```
In development, you'll want to use [`@babel/register`](https://babeljs.io/docs/en/next/babel-register):
```bash
node --require @babel/register <your-entry-file>
```
In production, you'll want to build your files with [`@babel/cli`](https://babeljs.io/docs/en/babel-cli). Suppose you are compiling a folder `src` and you wanted the output to go to a new folder `dist` with non-javascript files copied:
```bash
babel src --out-dir dist --copy-files
```
And have your `.babelrc` setup:
```json
{
"presets": [
["@babel/preset-env", {
"targets": {
"node": true
}
}]
]
}
```
## Troubleshooting
Check the [Troubleshooting Guide](docs/troubleshooting.md) or [Debugging Koa](docs/guide.md#debugging-koa) in
@ -228,97 +180,14 @@ See [AUTHORS](AUTHORS).
- [中文文档 v2.x](https://github.com/demopark/koa-docs-Zh-CN)
- __[#koajs]__ on freenode
## Job Board
Looking for a career upgrade?
<a href="https://astro.netlify.com/automattic"><img src="https://astro.netlify.com/static/automattic.png"></a>
<a href="https://astro.netlify.com/segment"><img src="https://astro.netlify.com/static/segment.png"></a>
<a href="https://astro.netlify.com/auth0"><img src="https://astro.netlify.com/static/auth0.png"/></a>
## Backers
Support us with a monthly donation and help us continue our activities.
<a href="https://opencollective.com/koajs/backer/0/website" target="_blank"><img src="https://opencollective.com/koajs/backer/0/avatar.svg"></a>
<a href="https://opencollective.com/koajs/backer/1/website" target="_blank"><img src="https://opencollective.com/koajs/backer/1/avatar.svg"></a>
<a href="https://opencollective.com/koajs/backer/2/website" target="_blank"><img src="https://opencollective.com/koajs/backer/2/avatar.svg"></a>
<a href="https://opencollective.com/koajs/backer/3/website" target="_blank"><img src="https://opencollective.com/koajs/backer/3/avatar.svg"></a>
<a href="https://opencollective.com/koajs/backer/4/website" target="_blank"><img src="https://opencollective.com/koajs/backer/4/avatar.svg"></a>
<a href="https://opencollective.com/koajs/backer/5/website" target="_blank"><img src="https://opencollective.com/koajs/backer/5/avatar.svg"></a>
<a href="https://opencollective.com/koajs/backer/6/website" target="_blank"><img src="https://opencollective.com/koajs/backer/6/avatar.svg"></a>
<a href="https://opencollective.com/koajs/backer/7/website" target="_blank"><img src="https://opencollective.com/koajs/backer/7/avatar.svg"></a>
<a href="https://opencollective.com/koajs/backer/8/website" target="_blank"><img src="https://opencollective.com/koajs/backer/8/avatar.svg"></a>
<a href="https://opencollective.com/koajs/backer/9/website" target="_blank"><img src="https://opencollective.com/koajs/backer/9/avatar.svg"></a>
<a href="https://opencollective.com/koajs/backer/10/website" target="_blank"><img src="https://opencollective.com/koajs/backer/10/avatar.svg"></a>
<a href="https://opencollective.com/koajs/backer/11/website" target="_blank"><img src="https://opencollective.com/koajs/backer/11/avatar.svg"></a>
<a href="https://opencollective.com/koajs/backer/12/website" target="_blank"><img src="https://opencollective.com/koajs/backer/12/avatar.svg"></a>
<a href="https://opencollective.com/koajs/backer/13/website" target="_blank"><img src="https://opencollective.com/koajs/backer/13/avatar.svg"></a>
<a href="https://opencollective.com/koajs/backer/14/website" target="_blank"><img src="https://opencollective.com/koajs/backer/14/avatar.svg"></a>
<a href="https://opencollective.com/koajs/backer/15/website" target="_blank"><img src="https://opencollective.com/koajs/backer/15/avatar.svg"></a>
<a href="https://opencollective.com/koajs/backer/16/website" target="_blank"><img src="https://opencollective.com/koajs/backer/16/avatar.svg"></a>
<a href="https://opencollective.com/koajs/backer/17/website" target="_blank"><img src="https://opencollective.com/koajs/backer/17/avatar.svg"></a>
<a href="https://opencollective.com/koajs/backer/18/website" target="_blank"><img src="https://opencollective.com/koajs/backer/18/avatar.svg"></a>
<a href="https://opencollective.com/koajs/backer/19/website" target="_blank"><img src="https://opencollective.com/koajs/backer/19/avatar.svg"></a>
<a href="https://opencollective.com/koajs/backer/20/website" target="_blank"><img src="https://opencollective.com/koajs/backer/20/avatar.svg"></a>
<a href="https://opencollective.com/koajs/backer/21/website" target="_blank"><img src="https://opencollective.com/koajs/backer/21/avatar.svg"></a>
<a href="https://opencollective.com/koajs/backer/22/website" target="_blank"><img src="https://opencollective.com/koajs/backer/22/avatar.svg"></a>
<a href="https://opencollective.com/koajs/backer/23/website" target="_blank"><img src="https://opencollective.com/koajs/backer/23/avatar.svg"></a>
<a href="https://opencollective.com/koajs/backer/24/website" target="_blank"><img src="https://opencollective.com/koajs/backer/24/avatar.svg"></a>
<a href="https://opencollective.com/koajs/backer/25/website" target="_blank"><img src="https://opencollective.com/koajs/backer/25/avatar.svg"></a>
<a href="https://opencollective.com/koajs/backer/26/website" target="_blank"><img src="https://opencollective.com/koajs/backer/26/avatar.svg"></a>
<a href="https://opencollective.com/koajs/backer/27/website" target="_blank"><img src="https://opencollective.com/koajs/backer/27/avatar.svg"></a>
<a href="https://opencollective.com/koajs/backer/28/website" target="_blank"><img src="https://opencollective.com/koajs/backer/28/avatar.svg"></a>
<a href="https://opencollective.com/koajs/backer/29/website" target="_blank"><img src="https://opencollective.com/koajs/backer/29/avatar.svg"></a>
## Sponsors
Become a sponsor and get your logo on our README on Github with a link to your site.
<a href="https://opencollective.com/koajs/sponsor/0/website" target="_blank"><img src="https://opencollective.com/koajs/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/koajs/sponsor/1/website" target="_blank"><img src="https://opencollective.com/koajs/sponsor/1/avatar.svg"></a>
<a href="https://opencollective.com/koajs/sponsor/2/website" target="_blank"><img src="https://opencollective.com/koajs/sponsor/2/avatar.svg"></a>
<a href="https://opencollective.com/koajs/sponsor/3/website" target="_blank"><img src="https://opencollective.com/koajs/sponsor/3/avatar.svg"></a>
<a href="https://opencollective.com/koajs/sponsor/4/website" target="_blank"><img src="https://opencollective.com/koajs/sponsor/4/avatar.svg"></a>
<a href="https://opencollective.com/koajs/sponsor/5/website" target="_blank"><img src="https://opencollective.com/koajs/sponsor/5/avatar.svg"></a>
<a href="https://opencollective.com/koajs/sponsor/6/website" target="_blank"><img src="https://opencollective.com/koajs/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/koajs/sponsor/7/website" target="_blank"><img src="https://opencollective.com/koajs/sponsor/7/avatar.svg"></a>
<a href="https://opencollective.com/koajs/sponsor/8/website" target="_blank"><img src="https://opencollective.com/koajs/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/koajs/sponsor/9/website" target="_blank"><img src="https://opencollective.com/koajs/sponsor/9/avatar.svg"></a>
<a href="https://opencollective.com/koajs/sponsor/10/website" target="_blank"><img src="https://opencollective.com/koajs/sponsor/10/avatar.svg"></a>
<a href="https://opencollective.com/koajs/sponsor/11/website" target="_blank"><img src="https://opencollective.com/koajs/sponsor/11/avatar.svg"></a>
<a href="https://opencollective.com/koajs/sponsor/12/website" target="_blank"><img src="https://opencollective.com/koajs/sponsor/12/avatar.svg"></a>
<a href="https://opencollective.com/koajs/sponsor/13/website" target="_blank"><img src="https://opencollective.com/koajs/sponsor/13/avatar.svg"></a>
<a href="https://opencollective.com/koajs/sponsor/14/website" target="_blank"><img src="https://opencollective.com/koajs/sponsor/14/avatar.svg"></a>
<a href="https://opencollective.com/koajs/sponsor/15/website" target="_blank"><img src="https://opencollective.com/koajs/sponsor/15/avatar.svg"></a>
<a href="https://opencollective.com/koajs/sponsor/16/website" target="_blank"><img src="https://opencollective.com/koajs/sponsor/16/avatar.svg"></a>
<a href="https://opencollective.com/koajs/sponsor/17/website" target="_blank"><img src="https://opencollective.com/koajs/sponsor/17/avatar.svg"></a>
<a href="https://opencollective.com/koajs/sponsor/18/website" target="_blank"><img src="https://opencollective.com/koajs/sponsor/18/avatar.svg"></a>
<a href="https://opencollective.com/koajs/sponsor/19/website" target="_blank"><img src="https://opencollective.com/koajs/sponsor/19/avatar.svg"></a>
<a href="https://opencollective.com/koajs/sponsor/20/website" target="_blank"><img src="https://opencollective.com/koajs/sponsor/20/avatar.svg"></a>
<a href="https://opencollective.com/koajs/sponsor/21/website" target="_blank"><img src="https://opencollective.com/koajs/sponsor/21/avatar.svg"></a>
<a href="https://opencollective.com/koajs/sponsor/22/website" target="_blank"><img src="https://opencollective.com/koajs/sponsor/22/avatar.svg"></a>
<a href="https://opencollective.com/koajs/sponsor/23/website" target="_blank"><img src="https://opencollective.com/koajs/sponsor/23/avatar.svg"></a>
<a href="https://opencollective.com/koajs/sponsor/24/website" target="_blank"><img src="https://opencollective.com/koajs/sponsor/24/avatar.svg"></a>
<a href="https://opencollective.com/koajs/sponsor/25/website" target="_blank"><img src="https://opencollective.com/koajs/sponsor/25/avatar.svg"></a>
<a href="https://opencollective.com/koajs/sponsor/26/website" target="_blank"><img src="https://opencollective.com/koajs/sponsor/26/avatar.svg"></a>
<a href="https://opencollective.com/koajs/sponsor/27/website" target="_blank"><img src="https://opencollective.com/koajs/sponsor/27/avatar.svg"></a>
<a href="https://opencollective.com/koajs/sponsor/28/website" target="_blank"><img src="https://opencollective.com/koajs/sponsor/28/avatar.svg"></a>
<a href="https://opencollective.com/koajs/sponsor/29/website" target="_blank"><img src="https://opencollective.com/koajs/sponsor/29/avatar.svg"></a>
# License
[MIT](https://github.com/koajs/koa/blob/master/LICENSE)
[npm-image]: https://img.shields.io/npm/v/koa.svg?style=flat-square
[npm-url]: https://www.npmjs.com/package/koa
[travis-image]: https://img.shields.io/travis/koajs/koa/master.svg?style=flat-square
[travis-url]: https://travis-ci.org/koajs/koa
[coveralls-image]: https://img.shields.io/codecov/c/github/koajs/koa.svg?style=flat-square
[coveralls-url]: https://codecov.io/github/koajs/koa?branch=master
[backers-image]: https://opencollective.com/koajs/backers/badge.svg?style=flat-square
[sponsors-image]: https://opencollective.com/koajs/sponsors/badge.svg?style=flat-square
[npm-image]: https://img.shields.io/npm/v/koa-lite.svg?style=flat-square
[npm-url]: https://www.npmjs.com/package/koa-lite
[travis-image]: https://img.shields.io/travis/nfp-projects/koa-lite/master.svg?style=flat-square
[travis-url]: https://travis-ci.org/nfp-projects/koa-lite
[gitter-image]: https://img.shields.io/gitter/room/koajs/koa.svg?style=flat-square
[gitter-url]: https://gitter.im/koajs/koa?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
[#koajs]: https://webchat.freenode.net/?channels=#koajs

View File

@ -66,29 +66,6 @@ ctx.state.user = await User.find(id);
Koa applications extend an internal [EventEmitter](https://nodejs.org/dist/latest-v11.x/docs/api/events.html). `ctx.app.emit` emits an event with a type, defined by the first argument. For each event you can hook up "listeners", which is a function that is called when the event is emitted. Consult the [error handling docs](https://koajs.com/#error-handling) for more information.
### ctx.cookies.get(name, [options])
Get cookie `name` with `options`:
- `signed` the cookie requested should be signed
Koa uses the [cookies](https://github.com/pillarjs/cookies) module where options are simply passed.
### ctx.cookies.set(name, value, [options])
Set cookie `name` to `value` with `options`:
- `maxAge` a number representing the milliseconds from Date.now() for expiry
- `signed` sign the cookie value
- `expires` a `Date` for cookie expiration
- `path` cookie path, `'/'` by default
- `domain` cookie domain
- `secure` secure cookie
- `httpOnly` server-accessible cookie, __true__ by default
- `overwrite` a boolean indicating whether to overwrite previously set cookies of the same name (__false__ by default). If this is true, all cookies set during the same request with the same name (regardless of path or domain) are filtered out of the Set-Cookie header when setting this cookie.
Koa uses the [cookies](https://github.com/pillarjs/cookies) module where options are simply passed.
### ctx.throw([status], [msg], [properties])
Helper method to throw an error with a `.status` property
@ -172,10 +149,6 @@ Koa uses [http-assert](https://github.com/jshttp/http-assert) for assertions.
- `ctx.ips`
- `ctx.subdomains`
- `ctx.is()`
- `ctx.accepts()`
- `ctx.acceptsEncodings()`
- `ctx.acceptsCharsets()`
- `ctx.acceptsLanguages()`
- `ctx.get()`
## Response aliases

View File

@ -112,7 +112,6 @@ app.listen(3000);
the following are supported:
- `app.env` defaulting to the __NODE_ENV__ or "development"
- `app.keys` array of signed cookie keys
- `app.proxy` when true proxy header fields will be trusted
- `app.subdomainOffset` offset of `.subdomains` to ignore [2]
@ -176,26 +175,6 @@ https.createServer(app.callback()).listen(3001);
Add the given middleware function to this application. See [Middleware](https://github.com/koajs/koa/wiki#middleware) for
more information.
## app.keys=
Set signed cookie keys.
These are passed to [KeyGrip](https://github.com/crypto-utils/keygrip),
however you may also pass your own `KeyGrip` instance. For
example the following are acceptable:
```js
app.keys = ['im a newer secret', 'i like turtle'];
app.keys = new KeyGrip(['im a newer secret', 'i like turtle'], 'sha256');
```
These keys may be rotated and are used when signing cookies
with the `{ signed: true }` option:
```js
ctx.cookies.set('name', 'tobi', { signed: true });
```
## app.context
`app.context` is the prototype from which `ctx` is created.

View File

@ -241,141 +241,6 @@ if (ctx.is('image/*')) {
}
```
### Content Negotiation
Koa's `request` object includes helpful content negotiation utilities powered by [accepts](http://github.com/expressjs/accepts) and [negotiator](https://github.com/federomero/negotiator). These utilities are:
- `request.accepts(types)`
- `request.acceptsEncodings(types)`
- `request.acceptsCharsets(charsets)`
- `request.acceptsLanguages(langs)`
If no types are supplied, __all__ acceptable types are returned.
If multiple types are supplied, the best match will be returned. If no matches are found, a `false` is returned, and you should send a `406 "Not Acceptable"` response to the client.
In the case of missing accept headers where any type is acceptable, the first type will be returned. Thus, the order of types you supply is important.
### request.accepts(types)
Check if the given `type(s)` is acceptable, returning the best match when true, otherwise `false`. The `type` value may be one or more mime type string
such as "application/json", the extension name
such as "json", or an array `["json", "html", "text/plain"]`.
```js
// Accept: text/html
ctx.accepts('html');
// => "html"
// Accept: text/*, application/json
ctx.accepts('html');
// => "html"
ctx.accepts('text/html');
// => "text/html"
ctx.accepts('json', 'text');
// => "json"
ctx.accepts('application/json');
// => "application/json"
// Accept: text/*, application/json
ctx.accepts('image/png');
ctx.accepts('png');
// => false
// Accept: text/*;q=.5, application/json
ctx.accepts(['html', 'json']);
ctx.accepts('html', 'json');
// => "json"
// No Accept header
ctx.accepts('html', 'json');
// => "html"
ctx.accepts('json', 'html');
// => "json"
```
You may call `ctx.accepts()` as many times as you like,
or use a switch:
```js
switch (ctx.accepts('json', 'html', 'text')) {
case 'json': break;
case 'html': break;
case 'text': break;
default: ctx.throw(406, 'json, html, or text only');
}
```
### request.acceptsEncodings(encodings)
Check if `encodings` are acceptable, returning the best match when true, otherwise `false`. Note that you should include `identity` as one of the encodings!
```js
// Accept-Encoding: gzip
ctx.acceptsEncodings('gzip', 'deflate', 'identity');
// => "gzip"
ctx.acceptsEncodings(['gzip', 'deflate', 'identity']);
// => "gzip"
```
When no arguments are given all accepted encodings
are returned as an array:
```js
// Accept-Encoding: gzip, deflate
ctx.acceptsEncodings();
// => ["gzip", "deflate", "identity"]
```
Note that the `identity` encoding (which means no encoding) could be unacceptable if the client explicitly sends `identity;q=0`. Although this is an edge case, you should still handle the case where this method returns `false`.
### request.acceptsCharsets(charsets)
Check if `charsets` are acceptable, returning
the best match when true, otherwise `false`.
```js
// Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5
ctx.acceptsCharsets('utf-8', 'utf-7');
// => "utf-8"
ctx.acceptsCharsets(['utf-7', 'utf-8']);
// => "utf-8"
```
When no arguments are given all accepted charsets
are returned as an array:
```js
// Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5
ctx.acceptsCharsets();
// => ["utf-8", "utf-7", "iso-8859-1"]
```
### request.acceptsLanguages(langs)
Check if `langs` are acceptable, returning
the best match when true, otherwise `false`.
```js
// Accept-Language: en;q=0.8, es, pt
ctx.acceptsLanguages('es', 'en');
// => "es"
ctx.acceptsLanguages(['en', 'es']);
// => "es"
```
When no arguments are given all accepted languages
are returned as an array:
```js
// Accept-Language: en;q=0.8, es, pt
ctx.acceptsLanguages();
// => ["es", "pt", "en"]
```
### request.idempotent
Check if the request is idempotent.

View File

@ -209,7 +209,7 @@ app.use(async function (ctx, next) {
## Debugging Koa
Koa along with many of the libraries it's built with support the __DEBUG__ environment variable from [debug](https://github.com/visionmedia/debug) which provides simple conditional logging.
Koa along with many of the libraries it's built with support the __DEBUG__ environment variable from [debug](https://github.com/nfp-projects/debug-ms) which provides simple conditional logging.
For example
to see all Koa-specific debugging information just pass `DEBUG=koa*` and upon boot you'll see the list of middleware used, among other things.

117
lib/accepts.js Normal file
View File

@ -0,0 +1,117 @@
const getMimetype = require('./getmimetype');
module.exports = function accepts(ctx, type, ask, isReq = true) {
if (!ctx._accept) {
ctx._accept = {};
}
// We don't need to parse content-type
if (!ctx._accept[type] && type !== 'content-type') {
let types = ctx.req.headers[type];
let quality = 9999; // Little bit of a hack :)
if (types) {
types = types.split(',')
.map(x => {
x = x.trim();
let q = quality--;
if (x.indexOf('q=') >= 0) {
q = parseFloat(x.substr(x.indexOf('q=') + 2)) || 1;
x = x.substr(0, x.indexOf(';'));
}
return [x, q];
})
.sort((a, b) => b[1] - a[1])
.map(x => x[0]);
} else {
types = [];
}
if (type === 'accept-encoding') {
types.push('identity');
}
ctx._accept[type] = types;
}
let can;
if (type === 'content-type') {
if (isReq) {
// Check if a request has a request body.
// A request with a body __must__ either have `transfer-encoding`
// or `content-length` headers set.
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3
if (ctx.req.headers['transfer-encoding'] === undefined
&& isNaN(ctx.req.headers['content-length'])) {
return null;
}
can = ctx.req.headers[type];
} else {
can = ctx.type;
}
} else {
can = ctx._accept[type];
if (!can.length) can = null;
}
// If empty argument, return all supported can
if (ask.length === 0 && can) {
return can || false;
}
// If no supported was sent, return the first ask item
// unless we're dealing with content-type we need to be smarter.
if (!can) {
if (type === 'content-type') {
return false;
}
return ask[0];
}
let parsed = ask.slice();
if (type === 'accept' || type === 'content-type') {
for (let t = 0; t < parsed.length; t++) {
if (parsed[t].startsWith('*/')) {
parsed[t] = parsed[t].substr(2);
} else if (parsed[t].indexOf('/*') < 0) {
parsed[t] = getMimetype(parsed[t]) || parsed[t];
}
}
if (type === 'content-type') {
can = [can.split(';')[0]];
}
}
// Loop over the supported can, returning the first
// matching ask type.
for (let i = 0; i < can.length; i++) {
for (let t = 0; t < parsed.length; t++) {
// Check if we allow root checking (application/*)
if (type === 'accept' || type === 'content-type') {
let allowRoot = can[i].indexOf('/*') >= 0
|| parsed[t].indexOf('/*') >= 0;
// Big if :)
if (can[i] === '*/*'
|| can[i].indexOf(parsed[t]) >= 0
|| (allowRoot
&& parsed[t].indexOf('/') >= 0
&& can[i].split('/')[0] === parsed[t].split('/')[0]
)) {
if (type === 'content-type') {
if (ask[t].indexOf('/') === -1) {
return ask[t];
}
return can[i];
}
return ask[t];
}
} else {
if (can[i] === parsed[t]) {
return ask[t];
}
}
}
}
return false;
};

View File

@ -5,22 +5,18 @@
* Module dependencies.
*/
const isGeneratorFunction = require('is-generator-function');
const debug = require('debug')('koa:application');
const onFinished = require('on-finished');
const response = require('./response');
const compose = require('koa-compose');
const isJSON = require('koa-is-json');
const context = require('./context');
const request = require('./request');
const statuses = require('statuses');
const debug = require('debug-ms')('koa:application');
const Emitter = require('events');
const util = require('util');
const Stream = require('stream');
const http = require('http');
const only = require('only');
const convert = require('koa-convert');
const deprecate = require('depd')('koa');
const onFinished = require('./onfinish');
const response = require('./response');
const compose = require('./compose');
const isJSON = require('./isjson');
const context = require('./context');
const request = require('./request');
const statuses = require('./statuses');
/**
* Expose `Application` class.
@ -38,19 +34,18 @@ module.exports = class Application extends Emitter {
*
* @param {object} [options] Application options
* @param {string} [options.env='development'] Environment
* @param {string[]} [options.keys] Signed cookie keys
* @param {boolean} [options.proxy] Trust proxy headers
* @param {number} [options.subdomainOffset] Subdomain offset
*
*/
constructor(options = {}) {
constructor(options) {
super();
options = options || {};
this.proxy = options.proxy || false;
this.middleware = [];
this.subdomainOffset = options.subdomainOffset || 2;
this.env = options.env || process.env.NODE_ENV || 'development';
this.keys = options.keys || undefined;
this.middleware = [];
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
@ -84,11 +79,11 @@ module.exports = class Application extends Emitter {
*/
toJSON() {
return only(this, [
'subdomainOffset',
'proxy',
'env'
]);
return {
subdomainOffset: this.subdomainOffset,
proxy: this.proxy,
env: this.env
};
}
/**
@ -114,12 +109,6 @@ module.exports = class Application extends Emitter {
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;

52
lib/compose.js Normal file
View File

@ -0,0 +1,52 @@
/**
* Lifted from koa-compose package.
*/
'use strict';
/**
* Compose `middleware` returning
* a fully valid middleware comprised
* of all those which are passed.
*
* @param {Array} middleware
* @return {Function}
* @api public
*/
function compose(middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!');
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!');
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
return function(context, next) {
// last called middleware #
let index = -1;
return dispatch(0);
function dispatch(i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'));
index = i;
let fn = middleware[i];
if (i === middleware.length) fn = next;
if (!fn) return Promise.resolve();
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err);
}
}
};
}
/**
* Expose compositor.
*/
module.exports = compose;

456
lib/content-disposition.js Normal file
View File

@ -0,0 +1,456 @@
/*!
* content-disposition
* Copyright(c) 2014-2017 Douglas Christopher Wilson
* MIT Licensed
*/
'use strict'
/**
* Module exports.
* @public
*/
module.exports = contentDisposition
module.exports.parse = parse
/**
* Module dependencies.
* @private
*/
const { basename } = require('path')
/**
* RegExp to match non attr-char, *after* encodeURIComponent (i.e. not including "%")
* @private
*/
const ENCODE_URL_ATTR_CHAR_REGEXP = /[\x00-\x20"'()*,/:;<=>?@[\\\]{}\x7f]/g // eslint-disable-line no-control-regex
/**
* RegExp to match percent encoding escape.
* @private
*/
const HEX_ESCAPE_REGEXP = /%[0-9A-Fa-f]{2}/
const HEX_ESCAPE_REPLACE_REGEXP = /%([0-9A-Fa-f]{2})/g
/**
* RegExp to match non-latin1 characters.
* @private
*/
const NON_LATIN1_REGEXP = /[^\x20-\x7e\xa0-\xff]/g
/**
* RegExp to match quoted-pair in RFC 2616
*
* quoted-pair = "\" CHAR
* CHAR = <any US-ASCII character (octets 0 - 127)>
* @private
*/
const QESC_REGEXP = /\\([\u0000-\u007f])/g // eslint-disable-line no-control-regex
/**
* RegExp to match chars that must be quoted-pair in RFC 2616
* @private
*/
const QUOTE_REGEXP = /([\\"])/g
/**
* RegExp for various RFC 2616 grammar
*
* parameter = token "=" ( token | quoted-string )
* token = 1*<any CHAR except CTLs or separators>
* separators = "(" | ")" | "<" | ">" | "@"
* | "," | ";" | ":" | "\" | <">
* | "/" | "[" | "]" | "?" | "="
* | "{" | "}" | SP | HT
* quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
* qdtext = <any TEXT except <">>
* quoted-pair = "\" CHAR
* CHAR = <any US-ASCII character (octets 0 - 127)>
* TEXT = <any OCTET except CTLs, but including LWS>
* LWS = [CRLF] 1*( SP | HT )
* CRLF = CR LF
* CR = <US-ASCII CR, carriage return (13)>
* LF = <US-ASCII LF, linefeed (10)>
* SP = <US-ASCII SP, space (32)>
* HT = <US-ASCII HT, horizontal-tab (9)>
* CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
* OCTET = <any 8-bit sequence of data>
* @private
*/
const PARAM_REGEXP = /;[\x09\x20]*([!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*=[\x09\x20]*("(?:[\x20!\x23-\x5b\x5d-\x7e\x80-\xff]|\\[\x20-\x7e])*"|[!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*/g // eslint-disable-line no-control-regex
const TEXT_REGEXP = /^[\x20-\x7e\x80-\xff]+$/
const TOKEN_REGEXP = /^[!#$%&'*+.0-9A-Z^_`a-z|~-]+$/
/**
* RegExp for various RFC 5987 grammar
*
* ext-value = charset "'" [ language ] "'" value-chars
* charset = "UTF-8" / "ISO-8859-1" / mime-charset
* mime-charset = 1*mime-charsetc
* mime-charsetc = ALPHA / DIGIT
* / "!" / "#" / "$" / "%" / "&"
* / "+" / "-" / "^" / "_" / "`"
* / "{" / "}" / "~"
* language = ( 2*3ALPHA [ extlang ] )
* / 4ALPHA
* / 5*8ALPHA
* extlang = *3( "-" 3ALPHA )
* value-chars = *( pct-encoded / attr-char )
* pct-encoded = "%" HEXDIG HEXDIG
* attr-char = ALPHA / DIGIT
* / "!" / "#" / "$" / "&" / "+" / "-" / "."
* / "^" / "_" / "`" / "|" / "~"
* @private
*/
const EXT_VALUE_REGEXP = /^([A-Za-z0-9!#$%&+\-^_`{}~]+)'(?:[A-Za-z]{2,3}(?:-[A-Za-z]{3}){0,3}|[A-Za-z]{4,8}|)'((?:%[0-9A-Fa-f]{2}|[A-Za-z0-9!#$&+.^_`|~-])+)$/
/**
* RegExp for various RFC 6266 grammar
*
* disposition-type = "inline" | "attachment" | disp-ext-type
* disp-ext-type = token
* disposition-parm = filename-parm | disp-ext-parm
* filename-parm = "filename" "=" value
* | "filename*" "=" ext-value
* disp-ext-parm = token "=" value
* | ext-token "=" ext-value
* ext-token = <the characters in token, followed by "*">
* @private
*/
const DISPOSITION_TYPE_REGEXP = /^([!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*(?:$|;)/ // eslint-disable-line no-control-regex
/**
* Create an attachment Content-Disposition header.
*
* @param {string} [filename]
* @param {object} [options]
* @param {string} [options.type=attachment]
* @param {string|boolean} [options.fallback=true]
* @return {string}
* @public
*/
function contentDisposition (filename, options) {
const opts = options || {}
// get type
const type = opts.type || 'attachment'
// get parameters
const params = createparams(filename, opts.fallback)
// format into string
return format(new ContentDisposition(type, params))
}
/**
* Create parameters object from filename and fallback.
*
* @param {string} [filename]
* @param {string|boolean} [fallback=true]
* @return {object}
* @private
*/
function createparams (filename, fallback) {
if (filename === undefined) {
return
}
const params = {}
if (typeof filename !== 'string') {
throw new TypeError('filename must be a string')
}
// fallback defaults to true
if (fallback === undefined) {
fallback = true
}
if (typeof fallback !== 'string' && typeof fallback !== 'boolean') {
throw new TypeError('fallback must be a string or boolean')
}
if (typeof fallback === 'string' && NON_LATIN1_REGEXP.test(fallback)) {
throw new TypeError('fallback must be ISO-8859-1 string')
}
// restrict to file base name
const name = basename(filename)
// determine if name is suitable for quoted string
const isQuotedString = TEXT_REGEXP.test(name)
// generate fallback name
const fallbackName = typeof fallback !== 'string'
? fallback && getlatin1(name)
: basename(fallback)
const hasFallback = typeof fallbackName === 'string' && fallbackName !== name
// set extended filename parameter
if (hasFallback || !isQuotedString || HEX_ESCAPE_REGEXP.test(name)) {
params['filename*'] = name
}
// set filename parameter
if (isQuotedString || hasFallback) {
params.filename = hasFallback
? fallbackName
: name
}
return params
}
/**
* Format object to Content-Disposition header.
*
* @param {object} obj
* @param {string} obj.type
* @param {object} [obj.parameters]
* @return {string}
* @private
*/
function format (obj) {
const parameters = obj.parameters
const type = obj.type
if (!type || typeof type !== 'string' || !TOKEN_REGEXP.test(type)) {
throw new TypeError('invalid type')
}
// start with normalized type
let string = String(type).toLowerCase()
// append parameters
if (parameters && typeof parameters === 'object') {
const params = Object.keys(parameters).sort()
for (let i = 0; i < params.length; i++) {
const param = params[i]
const val = param.substr(-1) === '*'
? ustring(parameters[param])
: qstring(parameters[param])
string += '; ' + param + '=' + val
}
}
return string
}
/**
* Decode a RFC 6987 field value (gracefully).
*
* @param {string} str
* @return {string}
* @private
*/
function decodefield (str) {
const match = EXT_VALUE_REGEXP.exec(str)
if (!match) {
throw new TypeError('invalid extended field value')
}
const charset = match[1].toLowerCase()
const encoded = match[2]
let value
// to binary string
const binary = encoded.replace(HEX_ESCAPE_REPLACE_REGEXP, pdecode)
switch (charset) {
case 'iso-8859-1':
value = getlatin1(binary)
break
case 'utf-8':
value = Buffer.from(binary, 'binary').toString('utf8')
break
default:
throw new TypeError('unsupported charset in extended field')
}
return value
}
/**
* Get ISO-8859-1 version of string.
*
* @param {string} val
* @return {string}
* @private
*/
function getlatin1 (val) {
// simple Unicode -> ISO-8859-1 transformation
return String(val).replace(NON_LATIN1_REGEXP, '?')
}
/**
* Parse Content-Disposition header string.
*
* @param {string} string
* @return {object}
* @public
*/
function parse (string) {
if (!string || typeof string !== 'string') {
throw new TypeError('argument string is required')
}
let match = DISPOSITION_TYPE_REGEXP.exec(string)
if (!match) {
throw new TypeError('invalid type format')
}
// normalize type
let index = match[0].length
const type = match[1].toLowerCase()
let key
let value
const names = []
const params = {}
// calculate index to start at
index = PARAM_REGEXP.lastIndex = match[0].substr(-1) === ';'
? index - 1
: index
// match parameters
while ((match = PARAM_REGEXP.exec(string))) {
if (match.index !== index) {
throw new TypeError('invalid parameter format')
}
index += match[0].length
key = match[1].toLowerCase()
value = match[2]
if (names.indexOf(key) !== -1) {
throw new TypeError('invalid duplicate parameter')
}
names.push(key)
if (key.indexOf('*') + 1 === key.length) {
// decode extended value
key = key.slice(0, -1)
value = decodefield(value)
// overwrite existing value
params[key] = value
continue
}
if (typeof params[key] === 'string') {
continue
}
if (value[0] === '"') {
// remove quotes and escapes
value = value
.substr(1, value.length - 2)
.replace(QESC_REGEXP, '$1')
}
params[key] = value
}
if (index !== -1 && index !== string.length) {
throw new TypeError('invalid parameter format')
}
return new ContentDisposition(type, params)
}
/**
* Percent decode a single character.
*
* @param {string} str
* @param {string} hex
* @return {string}
* @private
*/
function pdecode (str, hex) {
return String.fromCharCode(parseInt(hex, 16))
}
/**
* Percent encode a single character.
*
* @param {string} char
* @return {string}
* @private
*/
function pencode (char) {
return '%' + String(char)
.charCodeAt(0)
.toString(16)
.toUpperCase()
}
/**
* Quote a string for HTTP.
*
* @param {string} val
* @return {string}
* @private
*/
function qstring (val) {
const str = String(val)
return '"' + str.replace(QUOTE_REGEXP, '\\$1') + '"'
}
/**
* Encode a Unicode string for HTTP (RFC 5987).
*
* @param {string} val
* @return {string}
* @private
*/
function ustring (val) {
const str = String(val)
// percent encode as UTF-8
const encoded = encodeURIComponent(str)
.replace(ENCODE_URL_ATTR_CHAR_REGEXP, pencode)
return 'UTF-8\'\'' + encoded
}
/**
* Class for parsed Content-Disposition header for v8 optimization
*
* @public
* @param {string} type
* @param {object} parameters
* @constructor
*/
function ContentDisposition (type, parameters) {
this.type = type
this.parameters = parameters
}

View File

@ -6,13 +6,9 @@
*/
const util = require('util');
const createError = require('http-errors');
const httpAssert = require('http-assert');
const delegate = require('delegates');
const statuses = require('statuses');
const Cookies = require('cookies');
const COOKIES = Symbol('context#cookies');
const createError = require('http-errors-lite');
const delegate = require('./delegates');
const statuses = require('./statuses');
/**
* Context prototype.
@ -62,15 +58,17 @@ const proto = module.exports = {
*
* this.assert(this.user, 401, 'Please login!');
*
* See: https://github.com/jshttp/http-assert
*
* @param {Mixed} test
* @param {Number} status
* @param {String} message
* @api public
*/
assert: httpAssert,
assert: function(test, status, message, props) {
if (!test) {
this.throw(status, message, props);
}
},
/**
* Throw an error with `status` (default 500) and
@ -108,7 +106,7 @@ const proto = module.exports = {
// don't do anything if there is no error.
// this allows you to pass `this.onerror`
// to node-style callbacks.
if (null == err) return;
if (!err) return;
if (!(err instanceof Error)) err = new Error(util.format('non-error thrown: %j', err));
@ -155,20 +153,6 @@ const proto = module.exports = {
this.status = err.status;
this.length = Buffer.byteLength(msg);
res.end(msg);
},
get cookies() {
if (!this[COOKIES]) {
this[COOKIES] = new Cookies(this.req, this.res, {
keys: this.app.keys,
secure: this.request.secure
});
}
return this[COOKIES];
},
set cookies(_cookies) {
this[COOKIES] = _cookies;
}
};

157
lib/delegates.js Normal file
View File

@ -0,0 +1,157 @@
/**
* Expose `Delegator`.
*/
module.exports = Delegator;
/**
* Initialize a delegator.
*
* @param {Object} proto
* @param {String} target
* @api public
*/
function Delegator(proto, target) {
if (!(this instanceof Delegator)) return new Delegator(proto, target);
this.proto = proto;
this.target = target;
this.methods = [];
this.getters = [];
this.setters = [];
this.fluents = [];
}
/**
* Automatically delegate properties
* from a target prototype
*
* @param {Object} proto
* @param {object} targetProto
* @param {String} targetProp
* @api public
*/
Delegator.auto = function(proto, targetProto, targetProp) {
let delegator = Delegator(proto, targetProp);
let properties = Object.getOwnPropertyNames(targetProto);
for (let i = 0; i < properties.length; i++) {
let property = properties[i];
let descriptor = Object.getOwnPropertyDescriptor(targetProto, property);
if (descriptor.get) {
delegator.getter(property);
}
if (descriptor.set) {
delegator.setter(property);
}
if (descriptor.hasOwnProperty('value')) { // could be undefined but writable
let value = descriptor.value;
if (value instanceof Function) {
delegator.method(property);
} else {
delegator.getter(property);
}
if (descriptor.writable) {
delegator.setter(property);
}
}
}
};
/**
* Delegate method `name`.
*
* @param {String} name
* @return {Delegator} self
* @api public
*/
Delegator.prototype.method = function(name) {
let proto = this.proto;
let target = this.target;
this.methods.push(name);
proto[name] = function() {
return this[target][name].apply(this[target], arguments);
};
return this;
};
/**
* Delegator accessor `name`.
*
* @param {String} name
* @return {Delegator} self
* @api public
*/
Delegator.prototype.access = function(name) {
return this.getter(name).setter(name);
};
/**
* Delegator getter `name`.
*
* @param {String} name
* @return {Delegator} self
* @api public
*/
Delegator.prototype.getter = function(name) {
let proto = this.proto;
let target = this.target;
this.getters.push(name);
proto.__defineGetter__(name, function() {
return this[target][name];
});
return this;
};
/**
* Delegator setter `name`.
*
* @param {String} name
* @return {Delegator} self
* @api public
*/
Delegator.prototype.setter = function(name) {
let proto = this.proto;
let target = this.target;
this.setters.push(name);
proto.__defineSetter__(name, function(val) {
return this[target][name] = val;
});
return this;
};
/**
* Delegator fluent accessor
*
* @param {String} name
* @return {Delegator} self
* @api public
*/
Delegator.prototype.fluent = function(name) {
let proto = this.proto;
let target = this.target;
this.fluents.push(name);
proto[name] = function(val) {
if ('undefined' != typeof val) {
this[target][name] = val;
return this;
} else {
return this[target][name];
}
};
return this;
};

57
lib/fastparse.js Normal file
View File

@ -0,0 +1,57 @@
const url = require('url');
/**
* Parse the `str` url with fast-path short-cut.
*
* @param {string} str
* @return {Object}
* @private
*/
module.exports = function fastparse(str) {
if (typeof str !== 'string' || str.charCodeAt(0) !== 0x2f /* / */) {
return url.parse(str);
}
let pathname = str;
let query = null;
let search = null;
// This takes the regexp from https://github.com/joyent/node/pull/7878
// Which is /^(\/[^?#\s]*)(\?[^#\s]*)?$/
// And unrolls it into a for loop
for (let i = 1; i < str.length; i++) {
switch (str.charCodeAt(i)) {
case 0x3f: /* ? */
if (search === null) {
pathname = str.substring(0, i);
query = str.substring(i + 1);
search = str.substring(i);
}
break;
case 0x09: /* \t */
case 0x0a: /* \n */
case 0x0c: /* \f */
case 0x0d: /* \r */
case 0x20: /* */
case 0x23: /* # */
case 0xa0:
case 0xfeff:
return url.parse(str);
}
}
let parsed = new url.Url();
parsed.path = str;
parsed.href = str;
parsed.pathname = pathname;
if (search !== null) {
parsed.query = query;
parsed.search = search;
}
parsed.__raw = str;
return parsed;
};

137
lib/fresh.js Normal file
View File

@ -0,0 +1,137 @@
/*!
* fresh
* Copyright(c) 2012 TJ Holowaychuk
* Copyright(c) 2016-2017 Douglas Christopher Wilson
* MIT Licensed
*/
'use strict'
/**
* RegExp to check for no-cache token in Cache-Control.
* @private
*/
var CACHE_CONTROL_NO_CACHE_REGEXP = /(?:^|,)\s*?no-cache\s*?(?:,|$)/
/**
* Module exports.
* @public
*/
module.exports = fresh
/**
* Check freshness of the response using request and response headers.
*
* @param {Object} reqHeaders
* @param {Object} resHeaders
* @return {Boolean}
* @public
*/
function fresh (reqHeaders, resHeaders) {
// fields
var modifiedSince = reqHeaders['if-modified-since']
var noneMatch = reqHeaders['if-none-match']
// unconditional request
if (!modifiedSince && !noneMatch) {
return false
}
// Always return stale when Cache-Control: no-cache
// to support end-to-end reload requests
// https://tools.ietf.org/html/rfc2616#section-14.9.4
var cacheControl = reqHeaders['cache-control']
if (cacheControl && CACHE_CONTROL_NO_CACHE_REGEXP.test(cacheControl)) {
return false
}
// if-none-match
if (noneMatch && noneMatch !== '*') {
var etag = resHeaders['etag']
if (!etag) {
return false
}
var etagStale = true
var matches = parseTokenList(noneMatch)
for (var i = 0; i < matches.length; i++) {
var match = matches[i]
if (match === etag || match === 'W/' + etag || 'W/' + match === etag) {
etagStale = false
break
}
}
if (etagStale) {
return false
}
}
// if-modified-since
if (modifiedSince) {
var lastModified = resHeaders['last-modified']
var modifiedStale = !lastModified || !(parseHttpDate(lastModified) <= parseHttpDate(modifiedSince))
if (modifiedStale) {
return false
}
}
return true
}
/**
* Parse an HTTP Date into a number.
*
* @param {string} date
* @private
*/
function parseHttpDate (date) {
var timestamp = date && Date.parse(date)
// istanbul ignore next: guard against date.js Date.parse patching
return typeof timestamp === 'number'
? timestamp
: NaN
}
/**
* Parse a HTTP token list.
*
* @param {string} str
* @private
*/
function parseTokenList (str) {
var end = 0
var list = []
var start = 0
// gather tokens
for (var i = 0, len = str.length; i < len; i++) {
switch (str.charCodeAt(i)) {
case 0x20: /* */
if (start === end) {
start = end = i + 1
}
break
case 0x2c: /* , */
list.push(str.substring(start, end))
start = end = i + 1
break
default:
end = i + 1
break
}
}
// final token
list.push(str.substring(start, end))
return list
}

27
lib/getmimetype.js Normal file
View File

@ -0,0 +1,27 @@
module.exports = function getMimetype(type, includeCharset) {
let charset = includeCharset ? '; charset=utf-8' : '';
if (type.indexOf('json') >= 0 || type.indexOf('css.map') >= 0 || type.indexOf('js.map') >= 0) {
return 'application/json' + charset;
} else if (type.indexOf('html') >= 0) {
return 'text/html' + charset;
} else if (type.indexOf('css') >= 0) {
return 'text/css' + charset;
} else if (type.indexOf('js') >= 0 || type.indexOf('javascript') >= 0) {
return 'application/javascript' + charset;
} else if (type.indexOf('png') >= 0) {
return 'image/png';
} else if (type.indexOf('svg') >= 0) {
return 'image/svg+xml';
} else if (type.indexOf('jpg') >= 0) {
return 'image/jpeg';
} else if (type.indexOf('jpeg') >= 0) {
return 'image/jpeg';
} else if (type.indexOf('gif') >= 0) {
return 'image/gif';
} else if (type.indexOf('text') >= 0 || type.indexOf('txt') >= 0) {
return 'text/plain' + charset;
} else if (type.indexOf('bin') >= 0) {
return 'application/octet-stream';
}
};

13
lib/isjson.js Normal file
View File

@ -0,0 +1,13 @@
/**
* Check if `body` should be interpreted as json.
*/
function isJSON(body) {
if (!body) return false;
if ('string' == typeof body) return false;
if ('function' == typeof body.pipe) return false;
if (Buffer.isBuffer(body)) return false;
return true;
}
module.exports = isJSON;

74
lib/onfinish.js Normal file
View File

@ -0,0 +1,74 @@
/**
* Call callback when request finished. Lifted off of
* npm on-finished with slight optimizations.
*/
module.exports = function onFinished(msg, callback) {
let alreadyFinished = false;
// Make sure it hasn't finished already.
// Although I highly doubt this code is necessary.
if (typeof msg.finished === 'boolean') {
alreadyFinished = msg.finished || (msg.socket && !msg.socket.writable);
} else if (typeof msg.complete === 'boolean') {
alreadyFinished = msg.upgrade || !msg.socket || !msg.socket.readable || (msg.complete && !msg.readable);
} else {
// We don't support this object so end immediately
alreadyFinished = true;
}
if (alreadyFinished) {
return setImmediate(callback, null, msg);
}
if (msg.__onFinished) {
return msg.__onFinished.push(callback);
}
msg.__onFinished = [callback];
let socket = null;
let finished = false;
function onFinish(error) {
if (finished) return;
msg.removeListener('end', onFinish);
msg.removeListener('finish', onFinish);
if (socket) {
socket.removeListener('error', onFinish);
socket.removeListener('close', onFinish);
}
socket = null;
finished = true;
msg.__onFinished.forEach(cb => cb(error, msg));
}
msg.on('end', onFinish);
msg.on('finish', onFinish);
function onSocket(newSocket) {
// remove listener
msg.removeListener('socket', onSocket);
if (finished) return;
if (socket) return;
socket = newSocket;
// finished on first socket event
socket.on('error', onFinish);
socket.on('close', onFinish);
}
if (msg.socket) {
// socket already assigned
onSocket(msg.socket);
return;
}
// wait for socket to be assigned
msg.on('socket', onSocket);
};

View File

@ -7,15 +7,12 @@
const URL = require('url').URL;
const net = require('net');
const accepts = require('accepts');
const contentType = require('content-type');
const stringify = require('url').format;
const parse = require('parseurl');
const qs = require('querystring');
const typeis = require('type-is');
const fresh = require('fresh');
const only = require('only');
const util = require('util');
const fresh = require('./fresh');
const fastparse = require('./fastparse');
const accepts = require('./accepts');
const IP = Symbol('context#ip');
@ -142,7 +139,14 @@ module.exports = {
*/
get path() {
return parse(this.req).pathname;
return this.urlParsed.pathname;
},
get urlParsed() {
if (!this.req.__url || this.req.__url.__raw !== this.req.url) {
this.req.__url = fastparse(this.req.url);
}
return this.req.__url;
},
/**
@ -153,7 +157,7 @@ module.exports = {
*/
set path(path) {
const url = parse(this.req);
const url = this.urlParsed;
if (url.pathname === path) return;
url.pathname = path;
@ -195,7 +199,7 @@ module.exports = {
get querystring() {
if (!this.req) return '';
return parse(this.req).query || '';
return this.urlParsed.query || '';
},
/**
@ -206,7 +210,7 @@ module.exports = {
*/
set querystring(str) {
const url = parse(this.req);
const url = this.urlParsed;
if (url.search === `?${str}`) return;
url.search = str;
@ -357,22 +361,6 @@ module.exports = {
return this.req.socket;
},
/**
* Get the charset when present or undefined.
*
* @return {String}
* @api public
*/
get charset() {
try {
const { parameters } = contentType.parse(this.req);
return parameters.charset || '';
} catch (e) {
return '';
}
},
/**
* Return parsed Content-Length when present.
*
@ -484,27 +472,6 @@ module.exports = {
.slice(offset);
},
/**
* Get accept object.
* Lazily memoized.
*
* @return {Object}
* @api private
*/
get accept() {
return this._accept || (this._accept = accepts(this.req));
},
/**
* Set accept object.
*
* @param {Object}
* @api private
*/
set accept(obj) {
this._accept = obj;
},
/**
* Check if the given `type(s)` is acceptable, returning
* the best match when true, otherwise `false`, in which
@ -547,7 +514,14 @@ module.exports = {
*/
accepts(...args) {
return this.accept.types(...args);
let types = [...args];
// If passed an array, grab it
if (types[0] instanceof Array) {
types = types[0];
}
return accepts(this, 'accept', types);
},
/**
@ -564,7 +538,14 @@ module.exports = {
*/
acceptsEncodings(...args) {
return this.accept.encodings(...args);
let types = [...args];
// If passed an array, grab it
if (types[0] instanceof Array) {
types = types[0];
}
return accepts(this, 'accept-encoding', types);
},
/**
@ -581,7 +562,14 @@ module.exports = {
*/
acceptsCharsets(...args) {
return this.accept.charsets(...args);
let types = [...args];
// If passed an array, grab it
if (types[0] instanceof Array) {
types = types[0];
}
return accepts(this, 'accept-charset', types);
},
/**
@ -598,7 +586,14 @@ module.exports = {
*/
acceptsLanguages(...args) {
return this.accept.languages(...args);
let types = [...args];
// If passed an array, grab it
if (types[0] instanceof Array) {
types = types[0];
}
return accepts(this, 'accept-language', types);
},
/**
@ -628,9 +623,9 @@ module.exports = {
*/
is(types) {
if (!types) return typeis(this.req);
if (!types) return accepts(this, 'content-type', []);
if (!Array.isArray(types)) types = [].slice.call(arguments);
return typeis(this.req, types);
return accepts(this, 'content-type', types);
},
/**
@ -700,11 +695,11 @@ module.exports = {
*/
toJSON() {
return only(this, [
'method',
'url',
'header'
]);
return {
method: this.method,
url: this.url,
header: this.header
};
}
};

View File

@ -5,20 +5,16 @@
* Module dependencies.
*/
const contentDisposition = require('content-disposition');
const ensureErrorHandler = require('error-inject');
const getType = require('cache-content-type');
const onFinish = require('on-finished');
const isJSON = require('koa-is-json');
const escape = require('escape-html');
const typeis = require('type-is').is;
const statuses = require('statuses');
const destroy = require('destroy');
const ReadStream = require('fs').ReadStream;
const contentDisposition = require('./content-disposition');
const assert = require('assert');
const extname = require('path').extname;
const vary = require('vary');
const only = require('only');
const util = require('util');
const onFinish = require('./onfinish');
const isJSON = require('./isjson');
const statuses = require('./statuses');
const getMimetype = require('./getmimetype');
const accepts = require('./accepts');
/**
* Prototype.
@ -166,8 +162,25 @@ module.exports = {
// stream
if ('function' == typeof val.pipe) {
onFinish(this.res, destroy.bind(null, val));
ensureErrorHandler(val, err => this.ctx.onerror(err));
// On finish, destroy the stream
onFinish(this.res, () => {
// Functionality taken from destroy
if (!(val instanceof ReadStream)) {
if (typeof val.destroy === 'function') {
val.destroy();
}
return;
}
if (typeof val.close !== 'function') return;
// Fix potential bug (?) with node leaving file descriptor open
val.on('open', function() {
if (typeof this.fd === 'number') {
this.close();
}
});
});
val.on('error', err => this.ctx.onerror(err));
// overwriting
if (null != original && original != val) this.remove('Content-Length');
@ -235,7 +248,14 @@ module.exports = {
vary(field) {
if (this.headerSent) return;
vary(this.res, field);
// Revert #291, no reason to include full module
// that can be accomplished in 4 extra lines of code
let list = this.header.vary;
if (!list) return this.set('vary', field);
list = list.split(/ *, */);
if (!~list.indexOf(field)) list.push(field);
this.set('vary', list.join(', '));
},
/**
@ -260,14 +280,20 @@ module.exports = {
redirect(url, alt) {
// location
if ('back' == url) url = this.ctx.get('Referrer') || alt || '/';
this.set('Location', url);
this.set('Location', encodeURI(url));
// status
if (!statuses.redirect[this.status]) this.status = 302;
// html
if (this.ctx.accepts('html')) {
url = escape(url);
if (this.ctx.headers.accept && this.ctx.headers.accept.indexOf('html') >= 0) {
// Sanitize the url in case developer does something silly like:
// ctx.redirect(ctx.query.goto) or something without sanitizing himself.
url = url.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
this.type = 'text/html; charset=utf-8';
this.body = `Redirecting to <a href="${url}">${url}</a>.`;
return;
@ -306,12 +332,28 @@ module.exports = {
* @api public
*/
set type(type) {
type = getType(type);
if (type) {
set type(orgType) {
let type = orgType;
if (!type) {
this.remove('Content-Type');
return;
}
// If full type is specified, pass it straight on.
// Otherwise we do some basic checking for most common
// supported mime types.
if (type.indexOf('/') > 0 || type.indexOf(';') > 0) {
if (type.indexOf(';') === -1 && type.indexOf('text') >= 0) {
type += '; charset=utf-8';
}
this.set('Content-Type', type);
} else {
this.remove('Content-Type');
let mimetype = getMimetype(type, true);
if (mimetype) {
this.set('Content-Type', mimetype);
} else {
this.remove('Content-Type');
}
}
},
@ -394,10 +436,9 @@ module.exports = {
*/
is(types) {
const type = this.type;
if (!types) return type || false;
if (!types) return this.type || false;
if (!Array.isArray(types)) types = [].slice.call(arguments);
return typeis(type, types);
return accepts(this, 'content-type', types, false);
},
/**
@ -532,11 +573,11 @@ module.exports = {
*/
toJSON() {
return only(this, [
'status',
'message',
'header'
]);
return {
status: this.status,
message: this.message,
header: this.header
};
},
/**

81
lib/statuses.js Normal file
View File

@ -0,0 +1,81 @@
module.exports = {
100: 'Continue',
101: 'Switching Protocols',
102: 'Processing',
103: 'Early Hints',
200: 'OK',
201: 'Created',
202: 'Accepted',
203: 'Non-Authoritative Information',
204: 'No Content',
205: 'Reset Content',
206: 'Partial Content',
207: 'Multi-Status',
208: 'Already Reported',
226: 'IM Used',
300: 'Multiple Choices',
301: 'Moved Permanently',
302: 'Found',
303: 'See Other',
304: 'Not Modified',
305: 'Use Proxy',
306: '(Unused)',
307: 'Temporary Redirect',
308: 'Permanent Redirect',
400: 'Bad Request',
401: 'Unauthorized',
402: 'Payment Required',
403: 'Forbidden',
404: 'Not Found',
405: 'Method Not Allowed',
406: 'Not Acceptable',
407: 'Proxy Authentication Required',
408: 'Request Timeout',
409: 'Conflict',
410: 'Gone',
411: 'Length Required',
412: 'Precondition Failed',
413: 'Payload Too Large',
414: 'URI Too Long',
415: 'Unsupported Media Type',
416: 'Range Not Satisfiable',
417: 'Expectation Failed',
418: 'I\'m a teapot',
421: 'Misdirected Request',
422: 'Unprocessable Entity',
423: 'Locked',
424: 'Failed Dependency',
425: 'Too Early',
426: 'Upgrade Required',
428: 'Precondition Required',
429: 'Too Many Requests',
431: 'Request Header Fields Too Large',
451: 'Unavailable For Legal Reasons',
500: 'Internal Server Error',
501: 'Not Implemented',
502: 'Bad Gateway',
503: 'Service Unavailable',
504: 'Gateway Timeout',
505: 'HTTP Version Not Supported',
506: 'Variant Also Negotiates',
507: 'Insufficient Storage',
508: 'Loop Detected',
509: 'Bandwidth Limit Exceeded',
510: 'Not Extended',
511: 'Network Authentication Required',
redirect: {
300: true,
301: true,
302: true,
303: true,
305: true,
307: true,
308: true
},
empty: {
204: true,
205: true,
304: true
}
};

View File

@ -1,7 +1,7 @@
{
"name": "koa",
"version": "2.8.0",
"description": "Koa web app framework",
"name": "koa-lite",
"version": "2.10.1",
"description": "Lite version of the Koa web app framework",
"main": "lib/application.js",
"scripts": {
"test": "egg-bin test test",
@ -10,7 +10,7 @@
"bench": "make -C benchmarks",
"authors": "git log --format='%aN <%aE>' | sort -u > AUTHORS"
},
"repository": "koajs/koa",
"repository": "nfp-projects/koa-lite",
"keywords": [
"web",
"app",
@ -22,30 +22,8 @@
],
"license": "MIT",
"dependencies": {
"accepts": "^1.3.5",
"cache-content-type": "^1.0.0",
"content-disposition": "~0.5.2",
"content-type": "^1.0.4",
"cookies": "~0.7.1",
"debug": "~3.1.0",
"delegates": "^1.0.0",
"depd": "^1.1.2",
"destroy": "^1.0.4",
"error-inject": "^1.0.0",
"escape-html": "^1.0.3",
"fresh": "~0.5.2",
"http-assert": "^1.3.0",
"http-errors": "^1.6.3",
"is-generator-function": "^1.0.7",
"koa-compose": "^4.1.0",
"koa-convert": "^1.2.0",
"koa-is-json": "^1.0.0",
"on-finished": "^2.3.0",
"only": "~0.0.2",
"parseurl": "^1.3.2",
"statuses": "^1.5.0",
"type-is": "^1.6.16",
"vary": "^1.1.2"
"debug-ms": "~4.1.2",
"http-errors-lite": "^2.0.2"
},
"devDependencies": {
"egg-bin": "^4.13.0",

View File

@ -32,8 +32,7 @@ describe('app', () => {
ctx.socket.writable = false;
ctx.status = 204;
// throw if .writeHead or .end is called
ctx.res.writeHead =
ctx.res.end = () => {
ctx.res.writeHead = ctx.res.end = () => {
throw new Error('response sent');
};
});
@ -66,12 +65,6 @@ describe('app', () => {
assert.strictEqual(app.proxy, proxy);
});
it('should set signed cookie keys from the constructor', () => {
const keys = ['customkey'];
const app = new Koa({ keys });
assert.strictEqual(app.keys, keys);
});
it('should set subdomainOffset from the constructor', () => {
const subdomainOffset = 3;
const app = new Koa({ subdomainOffset });

View File

@ -2,7 +2,6 @@
'use strict';
const request = require('supertest');
const statuses = require('statuses');
const assert = require('assert');
const Koa = require('../..');
const fs = require('fs');
@ -20,6 +19,7 @@ describe('app.respond', () => {
res.statusCode = 200;
setImmediate(() => {
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Length', '3');
res.end('lol');
});
});
@ -41,6 +41,7 @@ describe('app.respond', () => {
const res = ctx.res;
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Length', '3');
res.end('lol');
ctx.set('foo', 'bar');
});
@ -65,6 +66,7 @@ describe('app.respond', () => {
const res = ctx.res;
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Length', '3');
res.end('lol');
ctx.status = 201;
});
@ -236,7 +238,6 @@ describe('app.respond', () => {
ctx.status = 200;
res.setHeader('Content-Type', 'text/html');
res.write('Hello');
setTimeout(() => res.end('Goodbye'), 0);
});
app.on('error', err => { throw err; });
@ -349,26 +350,6 @@ describe('app.respond', () => {
});
});
describe('with custom status=700', () => {
it('should respond with the associated status message', async() => {
const app = new Koa();
statuses['700'] = 'custom status';
app.use(ctx => {
ctx.status = 700;
});
const server = app.listen();
const res = await request(server)
.get('/')
.expect(700)
.expect('custom status');
assert.equal(res.res.statusMessage, 'custom status');
});
});
describe('with custom statusMessage=ok', () => {
it('should respond with the custom status message', async() => {
const app = new Koa();

View File

@ -40,64 +40,17 @@ describe('app.use(fn)', () => {
assert.deepEqual(calls, [1, 2, 3, 4, 5, 6]);
});
it('should compose mixed middleware', async() => {
process.once('deprecation', () => {}); // silence deprecation message
const app = new Koa();
const calls = [];
app.use((ctx, next) => {
calls.push(1);
return next().then(() => {
calls.push(6);
});
});
app.use(function * (next){
calls.push(2);
yield next;
calls.push(5);
});
app.use((ctx, next) => {
calls.push(3);
return next().then(() => {
calls.push(4);
});
});
const server = app.listen();
await request(server)
.get('/')
.expect(404);
assert.deepEqual(calls, [1, 2, 3, 4, 5, 6]);
});
// https://github.com/koajs/koa/pull/530#issuecomment-148138051
it('should catch thrown errors in non-async functions', () => {
const app = new Koa();
app.use(ctx => ctx.throw('Not Found', 404));
app.use(ctx => ctx.throw(404, 'Not Found'));
return request(app.callback())
.get('/')
.expect(404);
});
it('should accept both generator and function middleware', () => {
process.once('deprecation', () => {}); // silence deprecation message
const app = new Koa();
app.use((ctx, next) => next());
app.use(function * (next){ this.body = 'generator'; });
return request(app.callback())
.get('/')
.expect(200)
.expect('generator');
});
it('should throw error for non function', () => {
const app = new Koa();
@ -105,14 +58,4 @@ describe('app.use(fn)', () => {
assert.throws(() => app.use(v), /middleware must be a function!/);
});
});
it('should output deprecation message for generator functions', done => {
process.once('deprecation', message => {
assert(/Support for generators will be removed/.test(message));
done();
});
const app = new Koa();
app.use(function * (){});
});
});

View File

@ -1,119 +0,0 @@
'use strict';
const assert = require('assert');
const request = require('supertest');
const Koa = require('../..');
describe('ctx.cookies', () => {
describe('ctx.cookies.set()', () => {
it('should set an unsigned cookie', async() => {
const app = new Koa();
app.use((ctx, next) => {
ctx.cookies.set('name', 'jon');
ctx.status = 204;
});
const server = app.listen();
const res = await request(server)
.get('/')
.expect(204);
const cookie = res.headers['set-cookie'].some(cookie => /^name=/.test(cookie));
assert.equal(cookie, true);
});
describe('with .signed', () => {
describe('when no .keys are set', () => {
it('should error', () => {
const app = new Koa();
app.use((ctx, next) => {
try {
ctx.cookies.set('foo', 'bar', { signed: true });
} catch (err) {
ctx.body = err.message;
}
});
return request(app.callback())
.get('/')
.expect('.keys required for signed cookies');
});
});
it('should send a signed cookie', async() => {
const app = new Koa();
app.keys = ['a', 'b'];
app.use((ctx, next) => {
ctx.cookies.set('name', 'jon', { signed: true });
ctx.status = 204;
});
const server = app.listen();
const res = await request(server)
.get('/')
.expect(204);
const cookies = res.headers['set-cookie'];
assert.equal(cookies.some(cookie => /^name=/.test(cookie)), true);
assert.equal(cookies.some(cookie => /(,|^)name\.sig=/.test(cookie)), true);
});
});
describe('with secure', () => {
it('should get secure from request', async() => {
const app = new Koa();
app.proxy = true;
app.keys = ['a', 'b'];
app.use(ctx => {
ctx.cookies.set('name', 'jon', { signed: true });
ctx.status = 204;
});
const server = app.listen();
const res = await request(server)
.get('/')
.set('x-forwarded-proto', 'https') // mock secure
.expect(204);
const cookies = res.headers['set-cookie'];
assert.equal(cookies.some(cookie => /^name=/.test(cookie)), true);
assert.equal(cookies.some(cookie => /(,|^)name\.sig=/.test(cookie)), true);
assert.equal(cookies.every(cookie => /secure/.test(cookie)), true);
});
});
});
describe('ctx.cookies=', () => {
it('should override cookie work', async() => {
const app = new Koa();
app.use((ctx, next) => {
ctx.cookies = {
set(key, value){
ctx.set(key, value);
}
};
ctx.cookies.set('name', 'jon');
ctx.status = 204;
});
const server = app.listen();
await request(server)
.get('/')
.expect('name', 'jon')
.expect(204);
});
});
});

View File

@ -32,21 +32,6 @@ describe('ctx.throw(err)', () => {
});
});
describe('ctx.throw(err, status)', () => {
it('should throw the error and set .status', () => {
const ctx = context();
const error = new Error('test');
try {
ctx.throw(error, 422);
} catch (err) {
assert.equal(err.status, 422);
assert.equal(err.message, 'test');
assert.equal(err.expose, true);
}
});
});
describe('ctx.throw(status, err)', () => {
it('should throw the error and set .status', () => {
const ctx = context();
@ -62,20 +47,6 @@ describe('ctx.throw(status, err)', () => {
});
});
describe('ctx.throw(msg, status)', () => {
it('should throw an error', () => {
const ctx = context();
try {
ctx.throw('name required', 400);
} catch (err) {
assert.equal(err.message, 'name required');
assert.equal(err.status, 400);
assert.equal(err.expose, true);
}
});
});
describe('ctx.throw(status, msg)', () => {
it('should throw an error', () => {
const ctx = context();

View File

@ -1,27 +0,0 @@
'use strict';
const Accept = require('accepts');
const assert = require('assert');
const context = require('../helpers/context');
describe('ctx.accept', () => {
it('should return an Accept instance', () => {
const ctx = context();
ctx.req.headers.accept = 'application/*;q=0.2, image/jpeg;q=0.8, text/html, text/plain';
assert(ctx.accept instanceof Accept);
});
});
describe('ctx.accept=', () => {
it('should replace the accept object', () => {
const ctx = context();
ctx.req.headers.accept = 'text/plain';
assert.deepEqual(ctx.accepts(), ['text/plain']);
const request = context.request();
request.req.headers.accept = 'application/*;q=0.2, image/jpeg;q=0.8, text/html, text/plain';
ctx.accept = Accept(request.req);
assert.deepEqual(ctx.accepts(), ['text/html', 'text/plain', 'image/jpeg', 'application/*']);
});
});

View File

@ -1,36 +0,0 @@
'use strict';
const request = require('../helpers/context').request;
const assert = require('assert');
describe('req.charset', () => {
describe('with no content-type present', () => {
it('should return ""', () => {
const req = request();
assert('' === req.charset);
});
});
describe('with charset present', () => {
it('should return ""', () => {
const req = request();
req.header['content-type'] = 'text/plain';
assert('' === req.charset);
});
});
describe('with a charset', () => {
it('should return the charset', () => {
const req = request();
req.header['content-type'] = 'text/plain; charset=utf-8';
assert.equal(req.charset, 'utf-8');
});
it('should return "" if content-type is invalid', () => {
const req = request();
req.header['content-type'] = 'application/json; application/text; charset=utf-8';
assert.equal(req.charset, '');
});
});
});

View File

@ -3,6 +3,7 @@
const assert = require('assert');
const context = require('../helpers/context');
const fresh = require('../../lib/fresh')
describe('ctx.fresh', () => {
describe('the request method is not GET and HEAD', () => {
@ -48,3 +49,189 @@ describe('ctx.fresh', () => {
});
});
});
describe('fresh(reqHeaders, resHeaders)', function () {
describe('when a non-conditional GET is performed', function () {
it('should be stale', function () {
var reqHeaders = {}
var resHeaders = {}
assert.ok(!fresh(reqHeaders, resHeaders))
})
})
describe('when requested with If-None-Match', function () {
describe('when ETags match', function () {
it('should be fresh', function () {
var reqHeaders = { 'if-none-match': '"foo"' }
var resHeaders = { 'etag': '"foo"' }
assert.ok(fresh(reqHeaders, resHeaders))
})
})
describe('when ETags mismatch', function () {
it('should be stale', function () {
var reqHeaders = { 'if-none-match': '"foo"' }
var resHeaders = { 'etag': '"bar"' }
assert.ok(!fresh(reqHeaders, resHeaders))
})
})
describe('when at least one matches', function () {
it('should be fresh', function () {
var reqHeaders = { 'if-none-match': ' "bar" , "foo"' }
var resHeaders = { 'etag': '"foo"' }
assert.ok(fresh(reqHeaders, resHeaders))
})
})
describe('when etag is missing', function () {
it('should be stale', function () {
var reqHeaders = { 'if-none-match': '"foo"' }
var resHeaders = {}
assert.ok(!fresh(reqHeaders, resHeaders))
})
})
describe('when ETag is weak', function () {
it('should be fresh on exact match', function () {
var reqHeaders = { 'if-none-match': 'W/"foo"' }
var resHeaders = { 'etag': 'W/"foo"' }
assert.ok(fresh(reqHeaders, resHeaders))
})
it('should be fresh on strong match', function () {
var reqHeaders = { 'if-none-match': 'W/"foo"' }
var resHeaders = { 'etag': '"foo"' }
assert.ok(fresh(reqHeaders, resHeaders))
})
})
describe('when ETag is strong', function () {
it('should be fresh on exact match', function () {
var reqHeaders = { 'if-none-match': '"foo"' }
var resHeaders = { 'etag': '"foo"' }
assert.ok(fresh(reqHeaders, resHeaders))
})
it('should be fresh on weak match', function () {
var reqHeaders = { 'if-none-match': '"foo"' }
var resHeaders = { 'etag': 'W/"foo"' }
assert.ok(fresh(reqHeaders, resHeaders))
})
})
describe('when * is given', function () {
it('should be fresh', function () {
var reqHeaders = { 'if-none-match': '*' }
var resHeaders = { 'etag': '"foo"' }
assert.ok(fresh(reqHeaders, resHeaders))
})
it('should get ignored if not only value', function () {
var reqHeaders = { 'if-none-match': '*, "bar"' }
var resHeaders = { 'etag': '"foo"' }
assert.ok(!fresh(reqHeaders, resHeaders))
})
})
})
describe('when requested with If-Modified-Since', function () {
describe('when modified since the date', function () {
it('should be stale', function () {
var reqHeaders = { 'if-modified-since': 'Sat, 01 Jan 2000 00:00:00 GMT' }
var resHeaders = { 'last-modified': 'Sat, 01 Jan 2000 01:00:00 GMT' }
assert.ok(!fresh(reqHeaders, resHeaders))
})
})
describe('when unmodified since the date', function () {
it('should be fresh', function () {
var reqHeaders = { 'if-modified-since': 'Sat, 01 Jan 2000 01:00:00 GMT' }
var resHeaders = { 'last-modified': 'Sat, 01 Jan 2000 00:00:00 GMT' }
assert.ok(fresh(reqHeaders, resHeaders))
})
})
describe('when Last-Modified is missing', function () {
it('should be stale', function () {
var reqHeaders = { 'if-modified-since': 'Sat, 01 Jan 2000 00:00:00 GMT' }
var resHeaders = {}
assert.ok(!fresh(reqHeaders, resHeaders))
})
})
describe('with invalid If-Modified-Since date', function () {
it('should be stale', function () {
var reqHeaders = { 'if-modified-since': 'foo' }
var resHeaders = { 'last-modified': 'Sat, 01 Jan 2000 00:00:00 GMT' }
assert.ok(!fresh(reqHeaders, resHeaders))
})
})
describe('with invalid Last-Modified date', function () {
it('should be stale', function () {
var reqHeaders = { 'if-modified-since': 'Sat, 01 Jan 2000 00:00:00 GMT' }
var resHeaders = { 'last-modified': 'foo' }
assert.ok(!fresh(reqHeaders, resHeaders))
})
})
})
describe('when requested with If-Modified-Since and If-None-Match', function () {
describe('when both match', function () {
it('should be fresh', function () {
var reqHeaders = { 'if-none-match': '"foo"', 'if-modified-since': 'Sat, 01 Jan 2000 01:00:00 GMT' }
var resHeaders = { 'etag': '"foo"', 'last-modified': 'Sat, 01 Jan 2000 00:00:00 GMT' }
assert.ok(fresh(reqHeaders, resHeaders))
})
})
describe('when only ETag matches', function () {
it('should be stale', function () {
var reqHeaders = { 'if-none-match': '"foo"', 'if-modified-since': 'Sat, 01 Jan 2000 00:00:00 GMT' }
var resHeaders = { 'etag': '"foo"', 'last-modified': 'Sat, 01 Jan 2000 01:00:00 GMT' }
assert.ok(!fresh(reqHeaders, resHeaders))
})
})
describe('when only Last-Modified matches', function () {
it('should be stale', function () {
var reqHeaders = { 'if-none-match': '"foo"', 'if-modified-since': 'Sat, 01 Jan 2000 01:00:00 GMT' }
var resHeaders = { 'etag': '"bar"', 'last-modified': 'Sat, 01 Jan 2000 00:00:00 GMT' }
assert.ok(!fresh(reqHeaders, resHeaders))
})
})
describe('when none match', function () {
it('should be stale', function () {
var reqHeaders = { 'if-none-match': '"foo"', 'if-modified-since': 'Sat, 01 Jan 2000 00:00:00 GMT' }
var resHeaders = { 'etag': '"bar"', 'last-modified': 'Sat, 01 Jan 2000 01:00:00 GMT' }
assert.ok(!fresh(reqHeaders, resHeaders))
})
})
})
describe('when requested with Cache-Control: no-cache', function () {
it('should be stale', function () {
var reqHeaders = { 'cache-control': ' no-cache' }
var resHeaders = {}
assert.ok(!fresh(reqHeaders, resHeaders))
})
describe('when ETags match', function () {
it('should be stale', function () {
var reqHeaders = { 'cache-control': ' no-cache', 'if-none-match': '"foo"' }
var resHeaders = { 'etag': '"foo"' }
assert.ok(!fresh(reqHeaders, resHeaders))
})
})
describe('when unmodified since the date', function () {
it('should be stale', function () {
var reqHeaders = { 'cache-control': ' no-cache', 'if-modified-since': 'Sat, 01 Jan 2000 01:00:00 GMT' }
var resHeaders = { 'last-modified': 'Sat, 01 Jan 2000 00:00:00 GMT' }
assert.ok(!fresh(reqHeaders, resHeaders))
})
})
})
})

View File

@ -3,7 +3,6 @@
const assert = require('assert');
const context = require('../helpers/context');
const parseurl = require('parseurl');
describe('ctx.path', () => {
it('should return the pathname', () => {
@ -30,11 +29,4 @@ describe('ctx.path=', () => {
assert.equal(ctx.originalUrl, '/login');
assert.equal(ctx.request.originalUrl, '/login');
});
it('should not affect parseurl', () => {
const ctx = context({ url: '/login?foo=bar' });
ctx.path = '/login';
const url = parseurl(ctx.req);
assert.equal(url.path, '/login?foo=bar');
});
});

View File

@ -3,7 +3,6 @@
const assert = require('assert');
const context = require('../helpers/context');
const parseurl = require('parseurl');
describe('ctx.querystring', () => {
it('should return the querystring', () => {
@ -44,11 +43,4 @@ describe('ctx.querystring=', () => {
assert.equal(ctx.originalUrl, '/store/shoes');
assert.equal(ctx.request.originalUrl, '/store/shoes');
});
it('should not affect parseurl', () => {
const ctx = context({ url: '/login?foo=bar' });
ctx.querystring = 'foo=bar';
const url = parseurl(ctx.req);
assert.equal(url.path, '/login?foo=bar');
});
});

962
test/response/content.js Normal file
View File

@ -0,0 +1,962 @@
'use strict';
const assert = require('assert');
const contentDisposition = require('../../lib/content-disposition')
describe('contentDisposition()', function () {
it('should create an attachment header', function () {
assert.strictEqual(contentDisposition(), 'attachment')
})
})
describe('contentDisposition(filename)', function () {
it('should require a string', function () {
assert.throws(contentDisposition.bind(null, 42),
/filename.*string/)
})
it('should create a header with file name', function () {
assert.strictEqual(contentDisposition('plans.pdf'),
'attachment; filename="plans.pdf"')
})
it('should use the basename of the string', function () {
assert.strictEqual(contentDisposition('/path/to/plans.pdf'),
'attachment; filename="plans.pdf"')
})
describe('when "filename" is US-ASCII', function () {
it('should only include filename parameter', function () {
assert.strictEqual(contentDisposition('plans.pdf'),
'attachment; filename="plans.pdf"')
})
it('should escape quotes', function () {
assert.strictEqual(contentDisposition('the "plans".pdf'),
'attachment; filename="the \\"plans\\".pdf"')
})
})
describe('when "filename" is ISO-8859-1', function () {
it('should only include filename parameter', function () {
assert.strictEqual(contentDisposition('«plans».pdf'),
'attachment; filename="«plans».pdf"')
})
it('should escape quotes', function () {
assert.strictEqual(contentDisposition('the "plans" (1µ).pdf'),
'attachment; filename="the \\"plans\\" (1µ).pdf"')
})
})
describe('when "filename" is Unicode', function () {
it('should include filename* parameter', function () {
assert.strictEqual(contentDisposition('планы.pdf'),
'attachment; filename="?????.pdf"; filename*=UTF-8\'\'%D0%BF%D0%BB%D0%B0%D0%BD%D1%8B.pdf')
})
it('should include filename fallback', function () {
assert.strictEqual(contentDisposition('£ and € rates.pdf'),
'attachment; filename="£ and ? rates.pdf"; filename*=UTF-8\'\'%C2%A3%20and%20%E2%82%AC%20rates.pdf')
assert.strictEqual(contentDisposition('€ rates.pdf'),
'attachment; filename="? rates.pdf"; filename*=UTF-8\'\'%E2%82%AC%20rates.pdf')
})
it('should encode special characters', function () {
assert.strictEqual(contentDisposition('€\'*%().pdf'),
'attachment; filename="?\'*%().pdf"; filename*=UTF-8\'\'%E2%82%AC%27%2A%25%28%29.pdf')
})
})
describe('when "filename" contains hex escape', function () {
it('should include filename* parameter', function () {
assert.strictEqual(contentDisposition('the%20plans.pdf'),
'attachment; filename="the%20plans.pdf"; filename*=UTF-8\'\'the%2520plans.pdf')
})
it('should handle Unicode', function () {
assert.strictEqual(contentDisposition('€%20£.pdf'),
'attachment; filename="?%20£.pdf"; filename*=UTF-8\'\'%E2%82%AC%2520%C2%A3.pdf')
})
})
})
describe('contentDisposition(filename, options)', function () {
describe('with "fallback" option', function () {
it('should require a string or Boolean', function () {
assert.throws(contentDisposition.bind(null, 'plans.pdf', { fallback: 42 }),
/fallback.*string/)
})
it('should default to true', function () {
assert.strictEqual(contentDisposition('€ rates.pdf'),
'attachment; filename="? rates.pdf"; filename*=UTF-8\'\'%E2%82%AC%20rates.pdf')
})
describe('when "false"', function () {
it('should not generate ISO-8859-1 fallback', function () {
assert.strictEqual(contentDisposition('£ and € rates.pdf', { fallback: false }),
'attachment; filename*=UTF-8\'\'%C2%A3%20and%20%E2%82%AC%20rates.pdf')
})
it('should keep ISO-8859-1 filename', function () {
assert.strictEqual(contentDisposition('£ rates.pdf', { fallback: false }),
'attachment; filename="£ rates.pdf"')
})
})
describe('when "true"', function () {
it('should generate ISO-8859-1 fallback', function () {
assert.strictEqual(contentDisposition('£ and € rates.pdf', { fallback: true }),
'attachment; filename="£ and ? rates.pdf"; filename*=UTF-8\'\'%C2%A3%20and%20%E2%82%AC%20rates.pdf')
})
it('should pass through ISO-8859-1 filename', function () {
assert.strictEqual(contentDisposition('£ rates.pdf', { fallback: true }),
'attachment; filename="£ rates.pdf"')
})
})
describe('when a string', function () {
it('should require an ISO-8859-1 string', function () {
assert.throws(contentDisposition.bind(null, '€ rates.pdf', { fallback: '€ rates.pdf' }),
/fallback.*iso-8859-1/i)
})
it('should use as ISO-8859-1 fallback', function () {
assert.strictEqual(contentDisposition('£ and € rates.pdf', { fallback: '£ and EURO rates.pdf' }),
'attachment; filename="£ and EURO rates.pdf"; filename*=UTF-8\'\'%C2%A3%20and%20%E2%82%AC%20rates.pdf')
})
it('should use as fallback even when filename is ISO-8859-1', function () {
assert.strictEqual(contentDisposition('"£ rates".pdf', { fallback: '£ rates.pdf' }),
'attachment; filename="£ rates.pdf"; filename*=UTF-8\'\'%22%C2%A3%20rates%22.pdf')
})
it('should do nothing if equal to filename', function () {
assert.strictEqual(contentDisposition('plans.pdf', { fallback: 'plans.pdf' }),
'attachment; filename="plans.pdf"')
})
it('should use the basename of the string', function () {
assert.strictEqual(contentDisposition('€ rates.pdf', { fallback: '/path/to/EURO rates.pdf' }),
'attachment; filename="EURO rates.pdf"; filename*=UTF-8\'\'%E2%82%AC%20rates.pdf')
})
it('should do nothing without filename option', function () {
assert.strictEqual(contentDisposition(undefined, { fallback: 'plans.pdf' }),
'attachment')
})
})
})
describe('with "type" option', function () {
it('should default to attachment', function () {
assert.strictEqual(contentDisposition(),
'attachment')
})
it('should require a string', function () {
assert.throws(contentDisposition.bind(null, undefined, { type: 42 }),
/invalid type/)
})
it('should require a valid type', function () {
assert.throws(contentDisposition.bind(null, undefined, { type: 'invlaid;type' }),
/invalid type/)
})
it('should create a header with inline type', function () {
assert.strictEqual(contentDisposition(undefined, { type: 'inline' }),
'inline')
})
it('should create a header with inline type & filename', function () {
assert.strictEqual(contentDisposition('plans.pdf', { type: 'inline' }),
'inline; filename="plans.pdf"')
})
it('should normalize type', function () {
assert.strictEqual(contentDisposition(undefined, { type: 'INLINE' }),
'inline')
})
})
})
describe('contentDisposition.parse(string)', function () {
it('should require string', function () {
assert.throws(contentDisposition.parse.bind(null), /argument string.*required/)
})
it('should reject non-strings', function () {
assert.throws(contentDisposition.parse.bind(null, 42), /argument string.*required/)
})
describe('with only type', function () {
it('should reject quoted value', function () {
assert.throws(contentDisposition.parse.bind(null, '"attachment"'),
/invalid type format/)
})
it('should reject trailing semicolon', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment;'),
/invalid.*format/)
})
it('should parse "attachment"', function () {
assert.deepEqual(contentDisposition.parse('attachment'), {
type: 'attachment',
parameters: {}
})
})
it('should parse "inline"', function () {
assert.deepEqual(contentDisposition.parse('inline'), {
type: 'inline',
parameters: {}
})
})
it('should parse "form-data"', function () {
assert.deepEqual(contentDisposition.parse('form-data'), {
type: 'form-data',
parameters: {}
})
})
it('should parse with trailing LWS', function () {
assert.deepEqual(contentDisposition.parse('attachment \t '), {
type: 'attachment',
parameters: {}
})
})
it('should normalize to lower-case', function () {
assert.deepEqual(contentDisposition.parse('ATTACHMENT'), {
type: 'attachment',
parameters: {}
})
})
})
describe('with parameters', function () {
it('should reject trailing semicolon', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment; filename="rates.pdf";'),
/invalid parameter format/)
})
it('should reject invalid parameter name', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment; filename@="rates.pdf"'),
/invalid parameter format/)
})
it('should reject missing parameter value', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment; filename='),
/invalid parameter format/)
})
it('should reject invalid parameter value', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment; filename=trolly,trains'),
/invalid parameter format/)
})
it('should reject invalid parameters', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment; filename=total/; foo=bar'),
/invalid parameter format/)
})
it('should reject duplicate parameters', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment; filename=foo; filename=bar'),
/invalid duplicate parameter/)
})
it('should reject missing type', function () {
assert.throws(contentDisposition.parse.bind(null, 'filename="plans.pdf"'),
/invalid type format/)
assert.throws(contentDisposition.parse.bind(null, '; filename="plans.pdf"'),
/invalid type format/)
})
it('should lower-case parameter name', function () {
assert.deepEqual(contentDisposition.parse('attachment; FILENAME="plans.pdf"'), {
type: 'attachment',
parameters: { filename: 'plans.pdf' }
})
})
it('should parse quoted parameter value', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename="plans.pdf"'), {
type: 'attachment',
parameters: { filename: 'plans.pdf' }
})
})
it('should parse & unescape quoted value', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename="the \\"plans\\".pdf"'), {
type: 'attachment',
parameters: { filename: 'the "plans".pdf' }
})
})
it('should include all parameters', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename="plans.pdf"; foo=bar'), {
type: 'attachment',
parameters: { filename: 'plans.pdf', foo: 'bar' }
})
})
it('should parse parameters separated with any LWS', function () {
assert.deepEqual(contentDisposition.parse('attachment;filename="plans.pdf" \t; \t\t foo=bar'), {
type: 'attachment',
parameters: { filename: 'plans.pdf', foo: 'bar' }
})
})
it('should parse token filename', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename=plans.pdf'), {
type: 'attachment',
parameters: { filename: 'plans.pdf' }
})
})
it('should parse ISO-8859-1 filename', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename="£ rates.pdf"'), {
type: 'attachment',
parameters: { filename: '£ rates.pdf' }
})
})
})
describe('with extended parameters', function () {
it('should reject quoted extended parameter value', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment; filename*="UTF-8\'\'%E2%82%AC%20rates.pdf"'),
/invalid extended.*value/)
})
it('should parse UTF-8 extended parameter value', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename*=UTF-8\'\'%E2%82%AC%20rates.pdf'), {
type: 'attachment',
parameters: { 'filename': '€ rates.pdf' }
})
})
it('should parse UTF-8 extended parameter value', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename*=UTF-8\'\'%E2%82%AC%20rates.pdf'), {
type: 'attachment',
parameters: { 'filename': '€ rates.pdf' }
})
assert.deepEqual(contentDisposition.parse('attachment; filename*=UTF-8\'\'%E4%20rates.pdf'), {
type: 'attachment',
parameters: { 'filename': '\ufffd rates.pdf' }
})
})
it('should parse ISO-8859-1 extended parameter value', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename*=ISO-8859-1\'\'%A3%20rates.pdf'), {
type: 'attachment',
parameters: { 'filename': '£ rates.pdf' }
})
assert.deepEqual(contentDisposition.parse('attachment; filename*=ISO-8859-1\'\'%82%20rates.pdf'), {
type: 'attachment',
parameters: { 'filename': '? rates.pdf' }
})
})
it('should not be case-sensitive for charser', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename*=utf-8\'\'%E2%82%AC%20rates.pdf'), {
type: 'attachment',
parameters: { 'filename': '€ rates.pdf' }
})
})
it('should reject unsupported charset', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment; filename*=ISO-8859-2\'\'%A4%20rates.pdf'),
/unsupported charset/)
})
it('should parse with embedded language', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename*=UTF-8\'en\'%E2%82%AC%20rates.pdf'), {
type: 'attachment',
parameters: { 'filename': '€ rates.pdf' }
})
})
it('should prefer extended parameter value', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename="EURO rates.pdf"; filename*=UTF-8\'\'%E2%82%AC%20rates.pdf'), {
type: 'attachment',
parameters: { 'filename': '€ rates.pdf' }
})
assert.deepEqual(contentDisposition.parse('attachment; filename*=UTF-8\'\'%E2%82%AC%20rates.pdf; filename="EURO rates.pdf"'), {
type: 'attachment',
parameters: { 'filename': '€ rates.pdf' }
})
})
})
describe('from TC 2231', function () {
describe('Disposition-Type Inline', function () {
it('should parse "inline"', function () {
assert.deepEqual(contentDisposition.parse('inline'), {
type: 'inline',
parameters: {}
})
})
it('should reject ""inline""', function () {
assert.throws(contentDisposition.parse.bind(null, '"inline"'),
/invalid type format/)
})
it('should parse "inline; filename="foo.html""', function () {
assert.deepEqual(contentDisposition.parse('inline; filename="foo.html"'), {
type: 'inline',
parameters: { filename: 'foo.html' }
})
})
it('should parse "inline; filename="Not an attachment!""', function () {
assert.deepEqual(contentDisposition.parse('inline; filename="Not an attachment!"'), {
type: 'inline',
parameters: { filename: 'Not an attachment!' }
})
})
it('should parse "inline; filename="foo.pdf""', function () {
assert.deepEqual(contentDisposition.parse('inline; filename="foo.pdf"'), {
type: 'inline',
parameters: { filename: 'foo.pdf' }
})
})
})
describe('Disposition-Type Attachment', function () {
it('should parse "attachment"', function () {
assert.deepEqual(contentDisposition.parse('attachment'), {
type: 'attachment',
parameters: {}
})
})
it('should reject ""attachment""', function () {
assert.throws(contentDisposition.parse.bind(null, '"attachment"'),
/invalid type format/)
})
it('should parse "ATTACHMENT"', function () {
assert.deepEqual(contentDisposition.parse('ATTACHMENT'), {
type: 'attachment',
parameters: {}
})
})
it('should parse "attachment; filename="foo.html""', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename="foo.html"'), {
type: 'attachment',
parameters: { filename: 'foo.html' }
})
})
it('should parse "attachment; filename="0000000000111111111122222""', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename="0000000000111111111122222"'), {
type: 'attachment',
parameters: { filename: '0000000000111111111122222' }
})
})
it('should parse "attachment; filename="00000000001111111111222222222233333""', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename="00000000001111111111222222222233333"'), {
type: 'attachment',
parameters: { filename: '00000000001111111111222222222233333' }
})
})
it('should parse "attachment; filename="f\\oo.html""', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename="f\\oo.html"'), {
type: 'attachment',
parameters: { filename: 'foo.html' }
})
})
it('should parse "attachment; filename="\\"quoting\\" tested.html""', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename="\\"quoting\\" tested.html"'), {
type: 'attachment',
parameters: { filename: '"quoting" tested.html' }
})
})
it('should parse "attachment; filename="Here\'s a semicolon;.html""', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename="Here\'s a semicolon;.html"'), {
type: 'attachment',
parameters: { filename: 'Here\'s a semicolon;.html' }
})
})
it('should parse "attachment; foo="bar"; filename="foo.html""', function () {
assert.deepEqual(contentDisposition.parse('attachment; foo="bar"; filename="foo.html"'), {
type: 'attachment',
parameters: { filename: 'foo.html', foo: 'bar' }
})
})
it('should parse "attachment; foo="\\"\\\\";filename="foo.html""', function () {
assert.deepEqual(contentDisposition.parse('attachment; foo="\\"\\\\";filename="foo.html"'), {
type: 'attachment',
parameters: { filename: 'foo.html', foo: '"\\' }
})
})
it('should parse "attachment; FILENAME="foo.html""', function () {
assert.deepEqual(contentDisposition.parse('attachment; FILENAME="foo.html"'), {
type: 'attachment',
parameters: { filename: 'foo.html' }
})
})
it('should parse "attachment; filename=foo.html"', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename=foo.html'), {
type: 'attachment',
parameters: { filename: 'foo.html' }
})
})
it('should reject "attachment; filename=foo,bar.html"', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment; filename=foo,bar.html'),
/invalid parameter format/)
})
it('should reject "attachment; filename=foo.html ;"', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment; filename=foo.html ;'),
/invalid parameter format/)
})
it('should reject "attachment; ;filename=foo"', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment; ;filename=foo'),
/invalid parameter format/)
})
it('should reject "attachment; filename=foo bar.html"', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment; filename=foo bar.html'),
/invalid parameter format/)
})
it('should parse "attachment; filename=\'foo.bar\'', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename=\'foo.bar\''), {
type: 'attachment',
parameters: { filename: '\'foo.bar\'' }
})
})
it('should parse "attachment; filename="foo-ä.html""', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename="foo-ä.html"'), {
type: 'attachment',
parameters: { filename: 'foo-ä.html' }
})
})
it('should parse "attachment; filename="foo-ä.html""', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename="foo-ä.html"'), {
type: 'attachment',
parameters: { filename: 'foo-ä.html' }
})
})
it('should parse "attachment; filename="foo-%41.html""', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename="foo-%41.html"'), {
type: 'attachment',
parameters: { filename: 'foo-%41.html' }
})
})
it('should parse "attachment; filename="50%.html""', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename="50%.html"'), {
type: 'attachment',
parameters: { filename: '50%.html' }
})
})
it('should parse "attachment; filename="foo-%\\41.html""', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename="foo-%\\41.html"'), {
type: 'attachment',
parameters: { filename: 'foo-%41.html' }
})
})
it('should parse "attachment; name="foo-%41.html""', function () {
assert.deepEqual(contentDisposition.parse('attachment; name="foo-%41.html"'), {
type: 'attachment',
parameters: { name: 'foo-%41.html' }
})
})
it('should parse "attachment; filename="ä-%41.html""', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename="ä-%41.html"'), {
type: 'attachment',
parameters: { filename: 'ä-%41.html' }
})
})
it('should parse "attachment; filename="foo-%c3%a4-%e2%82%ac.html""', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename="foo-%c3%a4-%e2%82%ac.html"'), {
type: 'attachment',
parameters: { filename: 'foo-%c3%a4-%e2%82%ac.html' }
})
})
it('should parse "attachment; filename ="foo.html""', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename ="foo.html"'), {
type: 'attachment',
parameters: { filename: 'foo.html' }
})
})
it('should reject "attachment; filename="foo.html"; filename="bar.html"', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment; filename="foo.html"; filename="bar.html"'),
/invalid duplicate parameter/)
})
it('should reject "attachment; filename=foo[1](2).html"', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment; filename=foo[1](2).html'),
/invalid parameter format/)
})
it('should reject "attachment; filename=foo-ä.html"', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment; filename=foo-ä.html'),
/invalid parameter format/)
})
it('should reject "attachment; filename=foo-ä.html"', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment; filename=foo-ä.html'),
/invalid parameter format/)
})
it('should reject "filename=foo.html"', function () {
assert.throws(contentDisposition.parse.bind(null, 'filename=foo.html'),
/invalid type format/)
})
it('should reject "x=y; filename=foo.html"', function () {
assert.throws(contentDisposition.parse.bind(null, 'x=y; filename=foo.html'),
/invalid type format/)
})
it('should reject ""foo; filename=bar;baz"; filename=qux"', function () {
assert.throws(contentDisposition.parse.bind(null, '"foo; filename=bar;baz"; filename=qux'),
/invalid type format/)
})
it('should reject "filename=foo.html, filename=bar.html"', function () {
assert.throws(contentDisposition.parse.bind(null, 'filename=foo.html, filename=bar.html'),
/invalid type format/)
})
it('should reject "; filename=foo.html"', function () {
assert.throws(contentDisposition.parse.bind(null, '; filename=foo.html'),
/invalid type format/)
})
it('should reject ": inline; attachment; filename=foo.html', function () {
assert.throws(contentDisposition.parse.bind(null, ': inline; attachment; filename=foo.html'),
/invalid type format/)
})
it('should reject "inline; attachment; filename=foo.html', function () {
assert.throws(contentDisposition.parse.bind(null, 'inline; attachment; filename=foo.html'),
/invalid parameter format/)
})
it('should reject "attachment; inline; filename=foo.html', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment; inline; filename=foo.html'),
/invalid parameter format/)
})
it('should reject "attachment; filename="foo.html".txt', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment; filename="foo.html".txt'),
/invalid parameter format/)
})
it('should reject "attachment; filename="bar', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment; filename="bar'),
/invalid parameter format/)
})
it('should reject "attachment; filename=foo"bar;baz"qux', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment; filename=foo"bar;baz"qux'),
/invalid parameter format/)
})
it('should reject "attachment; filename=foo.html, attachment; filename=bar.html', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment; filename=foo.html, attachment; filename=bar.html'),
/invalid parameter format/)
})
it('should reject "attachment; foo=foo filename=bar', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment; foo=foo filename=bar'),
/invalid parameter format/)
})
it('should reject "attachment; filename=bar foo=foo', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment; filename=bar foo=foo'),
/invalid parameter format/)
})
it('should reject "attachment filename=bar', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment filename=bar'),
/invalid type format/)
})
it('should reject "filename=foo.html; attachment', function () {
assert.throws(contentDisposition.parse.bind(null, 'filename=foo.html; attachment'),
/invalid type format/)
})
it('should parse "attachment; xfilename=foo.html"', function () {
assert.deepEqual(contentDisposition.parse('attachment; xfilename=foo.html'), {
type: 'attachment',
parameters: { xfilename: 'foo.html' }
})
})
it('should parse "attachment; filename="/foo.html""', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename="/foo.html"'), {
type: 'attachment',
parameters: { filename: '/foo.html' }
})
})
it('should parse "attachment; filename="\\\\foo.html""', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename="\\\\foo.html"'), {
type: 'attachment',
parameters: { filename: '\\foo.html' }
})
})
})
describe('Additional Parameters', function () {
it('should parse "attachment; creation-date="Wed, 12 Feb 1997 16:29:51 -0500""', function () {
assert.deepEqual(contentDisposition.parse('attachment; creation-date="Wed, 12 Feb 1997 16:29:51 -0500"'), {
type: 'attachment',
parameters: { 'creation-date': 'Wed, 12 Feb 1997 16:29:51 -0500' }
})
})
it('should parse "attachment; modification-date="Wed, 12 Feb 1997 16:29:51 -0500""', function () {
assert.deepEqual(contentDisposition.parse('attachment; modification-date="Wed, 12 Feb 1997 16:29:51 -0500"'), {
type: 'attachment',
parameters: { 'modification-date': 'Wed, 12 Feb 1997 16:29:51 -0500' }
})
})
})
describe('Disposition-Type Extension', function () {
it('should parse "foobar"', function () {
assert.deepEqual(contentDisposition.parse('foobar'), {
type: 'foobar',
parameters: {}
})
})
it('should parse "attachment; example="filename=example.txt""', function () {
assert.deepEqual(contentDisposition.parse('attachment; example="filename=example.txt"'), {
type: 'attachment',
parameters: { example: 'filename=example.txt' }
})
})
})
describe('RFC 2231/5987 Encoding: Character Sets', function () {
it('should parse "attachment; filename*=iso-8859-1\'\'foo-%E4.html"', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename*=iso-8859-1\'\'foo-%E4.html'), {
type: 'attachment',
parameters: { filename: 'foo-ä.html' }
})
})
it('should parse "attachment; filename*=UTF-8\'\'foo-%c3%a4-%e2%82%ac.html"', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename*=UTF-8\'\'foo-%c3%a4-%e2%82%ac.html'), {
type: 'attachment',
parameters: { filename: 'foo-ä-€.html' }
})
})
it('should reject "attachment; filename*=\'\'foo-%c3%a4-%e2%82%ac.html"', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment; filename*=\'\'foo-%c3%a4-%e2%82%ac.html'),
/invalid extended.*value/)
})
it('should parse "attachment; filename*=UTF-8\'\'foo-a%cc%88.html"', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename*=UTF-8\'\'foo-a%cc%88.html'), {
type: 'attachment',
parameters: { filename: 'foo-ä.html' }
})
})
it('should parse "attachment; filename*=iso-8859-1\'\'foo-%c3%a4-%e2%82%ac.html"', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename*=iso-8859-1\'\'foo-%c3%a4-%e2%82%ac.html'), {
type: 'attachment',
parameters: { filename: 'foo-ä-�.html' }
})
})
it('should parse "attachment; filename*=utf-8\'\'foo-%E4.html"', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename*=utf-8\'\'foo-%E4.html'), {
type: 'attachment',
parameters: { filename: 'foo-\ufffd.html' }
})
})
it('should reject "attachment; filename *=UTF-8\'\'foo-%c3%a4.html"', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment; filename *=UTF-8\'\'foo-%c3%a4.html'),
/invalid parameter format/)
})
it('should parse "attachment; filename*= UTF-8\'\'foo-%c3%a4.html"', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename*= UTF-8\'\'foo-%c3%a4.html'), {
type: 'attachment',
parameters: { filename: 'foo-ä.html' }
})
})
it('should parse "attachment; filename* =UTF-8\'\'foo-%c3%a4.html"', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename* =UTF-8\'\'foo-%c3%a4.html'), {
type: 'attachment',
parameters: { filename: 'foo-ä.html' }
})
})
it('should reject "attachment; filename*="UTF-8\'\'foo-%c3%a4.html""', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment; filename*="UTF-8\'\'foo-%c3%a4.html"'),
/invalid extended field value/)
})
it('should reject "attachment; filename*="foo%20bar.html""', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment; filename*="foo%20bar.html"'),
/invalid extended field value/)
})
it('should reject "attachment; filename*=UTF-8\'foo-%c3%a4.html"', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment; filename*=UTF-8\'foo-%c3%a4.html'),
/invalid extended field value/)
})
it('should reject "attachment; filename*=UTF-8\'\'foo%"', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment; filename*=UTF-8\'\'foo%'),
/invalid extended field value/)
})
it('should reject "attachment; filename*=UTF-8\'\'f%oo.html"', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment; filename*=UTF-8\'\'f%oo.html'),
/invalid extended field value/)
})
it('should parse "attachment; filename*=UTF-8\'\'A-%2541.html"', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename*=UTF-8\'\'A-%2541.html'), {
type: 'attachment',
parameters: { filename: 'A-%41.html' }
})
})
it('should parse "attachment; filename*=UTF-8\'\'%5cfoo.html"', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename*=UTF-8\'\'%5cfoo.html'), {
type: 'attachment',
parameters: { filename: '\\foo.html' }
})
})
})
describe('RFC2231 Encoding: Continuations', function () {
it('should parse "attachment; filename*0="foo."; filename*1="html""', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename*0="foo."; filename*1="html"'), {
type: 'attachment',
parameters: { 'filename*0': 'foo.', 'filename*1': 'html' }
})
})
it('should parse "attachment; filename*0="foo"; filename*1="\\b\\a\\r.html""', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename*0="foo"; filename*1="\\b\\a\\r.html"'), {
type: 'attachment',
parameters: { 'filename*0': 'foo', 'filename*1': 'bar.html' }
})
})
it('should parse "attachment; filename*0*=UTF-8\'\'foo-%c3%a4; filename*1=".html""', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename*0*=UTF-8\'\'foo-%c3%a4; filename*1=".html"'), {
type: 'attachment',
parameters: { 'filename*0*': 'UTF-8\'\'foo-%c3%a4', 'filename*1': '.html' }
})
})
it('should parse "attachment; filename*0="foo"; filename*01="bar""', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename*0="foo"; filename*01="bar"'), {
type: 'attachment',
parameters: { 'filename*0': 'foo', 'filename*01': 'bar' }
})
})
it('should parse "attachment; filename*0="foo"; filename*2="bar""', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename*0="foo"; filename*2="bar"'), {
type: 'attachment',
parameters: { 'filename*0': 'foo', 'filename*2': 'bar' }
})
})
it('should parse "attachment; filename*1="foo."; filename*2="html""', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename*1="foo."; filename*2="html"'), {
type: 'attachment',
parameters: { 'filename*1': 'foo.', 'filename*2': 'html' }
})
})
it('should parse "attachment; filename*1="bar"; filename*0="foo""', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename*1="bar"; filename*0="foo"'), {
type: 'attachment',
parameters: { 'filename*1': 'bar', 'filename*0': 'foo' }
})
})
})
describe('RFC2231 Encoding: Fallback Behaviour', function () {
it('should parse "attachment; filename="foo-ae.html"; filename*=UTF-8\'\'foo-%c3%a4.html"', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename="foo-ae.html"; filename*=UTF-8\'\'foo-%c3%a4.html'), {
type: 'attachment',
parameters: { filename: 'foo-ä.html' }
})
})
it('should parse "attachment; filename*=UTF-8\'\'foo-%c3%a4.html; filename="foo-ae.html"', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename*=UTF-8\'\'foo-%c3%a4.html; filename="foo-ae.html"'), {
type: 'attachment',
parameters: { filename: 'foo-ä.html' }
})
})
it('should parse "attachment; filename*0*=ISO-8859-15\'\'euro-sign%3d%a4; filename*=ISO-8859-1\'\'currency-sign%3d%a4', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename*0*=ISO-8859-15\'\'euro-sign%3d%a4; filename*=ISO-8859-1\'\'currency-sign%3d%a4'), {
type: 'attachment',
parameters: { filename: 'currency-sign=¤', 'filename*0*': 'ISO-8859-15\'\'euro-sign%3d%a4' }
})
})
it('should parse "attachment; foobar=x; filename="foo.html"', function () {
assert.deepEqual(contentDisposition.parse('attachment; foobar=x; filename="foo.html"'), {
type: 'attachment',
parameters: { filename: 'foo.html', foobar: 'x' }
})
})
})
describe('RFC2047 Encoding', function () {
it('should reject "attachment; filename==?ISO-8859-1?Q?foo-=E4.html?="', function () {
assert.throws(contentDisposition.parse.bind(null, 'attachment; filename==?ISO-8859-1?Q?foo-=E4.html?='),
/invalid parameter format/)
})
it('should parse "attachment; filename="=?ISO-8859-1?Q?foo-=E4.html?=""', function () {
assert.deepEqual(contentDisposition.parse('attachment; filename="=?ISO-8859-1?Q?foo-=E4.html?="'), {
type: 'attachment',
parameters: { filename: '=?ISO-8859-1?Q?foo-=E4.html?=' }
})
})
})
})
})

View File

@ -2,7 +2,9 @@
'use strict';
const assert = require('assert');
const request = require('supertest');
const context = require('../helpers/context');
const Koa = require('../..');
describe('ctx.redirect(url)', () => {
it('should redirect to the given url', () => {
@ -12,6 +14,23 @@ describe('ctx.redirect(url)', () => {
assert.equal(ctx.status, 302);
});
it('should auto fix not encode url', done => {
const app = new Koa();
app.use(ctx => {
ctx.redirect('http://google.com/😓');
});
request(app.callback())
.get('/')
.end((err, res) => {
if (err) return done(err);
assert.equal(res.status, 302);
assert.equal(res.headers.location, 'http://google.com/%F0%9F%98%93');
done();
});
});
describe('with "back"', () => {
it('should redirect to Referrer', () => {
const ctx = context();

View File

@ -3,7 +3,6 @@
const response = require('../helpers/context').response;
const request = require('supertest');
const statuses = require('statuses');
const assert = require('assert');
const Koa = require('../..');
@ -29,20 +28,6 @@ describe('res.status=', () => {
});
});
describe('and custom status', () => {
beforeEach(() => statuses['700'] = 'custom status');
it('should set the status', () => {
const res = response();
res.status = 700;
assert.equal(res.status, 700);
});
it('should not throw', () => {
response().status = 700;
});
});
describe('and HTTP/2', () => {
it('should not set the status message', () => {
const res = response({

354
test/utils/compose.js Normal file
View File

@ -0,0 +1,354 @@
'use strict';
/* eslint-env jest */
const compose = require('../../lib/compose');
const assert = require('assert');
function wait(ms){
return new Promise(resolve => setTimeout(resolve, ms || 1));
}
function isPromise(x){
return x && typeof x.then === 'function';
}
describe('Koa Compose', () => {
it('should work', async() => {
const arr = [];
const stack = [];
stack.push(async(context, next) => {
arr.push(1);
await wait(1);
await next();
await wait(1);
arr.push(6);
});
stack.push(async(context, next) => {
arr.push(2);
await wait(1);
await next();
await wait(1);
arr.push(5);
});
stack.push(async(context, next) => {
arr.push(3);
await wait(1);
await next();
await wait(1);
arr.push(4);
});
await compose(stack)({});
assert.deepEqual(arr, [1, 2, 3, 4, 5, 6]);
});
it('should be able to be called twice', () => {
let stack = [];
stack.push(async(context, next) => {
context.arr.push(1);
await wait(1);
await next();
await wait(1);
context.arr.push(6);
});
stack.push(async(context, next) => {
context.arr.push(2);
await wait(1);
await next();
await wait(1);
context.arr.push(5);
});
stack.push(async(context, next) => {
context.arr.push(3);
await wait(1);
await next();
await wait(1);
context.arr.push(4);
});
const fn = compose(stack);
const ctx1 = { arr: [] };
const ctx2 = { arr: [] };
const out = [1, 2, 3, 4, 5, 6];
return fn(ctx1).then(() => {
assert.deepEqual(out, ctx1.arr);
return fn(ctx2);
}).then(() => {
assert.deepEqual(out, ctx2.arr);
});
});
it('should only accept an array', () => {
let err;
try {
compose();
throw new Error('should not be called');
} catch (e) {
err = e;
}
return assert(err instanceof TypeError);
});
it('should create next functions that return a Promise', () => {
const stack = [];
const arr = [];
for (let i = 0; i < 5; i++) {
stack.push((context, next) => {
arr.push(next());
});
}
compose(stack)({});
for (let next of arr) {
assert(isPromise(next), 'one of the functions next is not a Promise');
}
});
it('should work with 0 middleware', () => {
return compose([])({});
});
it('should only accept middleware as functions', () => {
let err;
try {
compose([{}]);
throw new Error('should not be called');
} catch (e) {
err = e;
}
return assert(err instanceof TypeError);
});
it('should work when yielding at the end of the stack', async() => {
let stack = [];
let called = false;
stack.push(async(ctx, next) => {
await next();
called = true;
});
await compose(stack)({});
assert(called);
});
it('should reject on errors in middleware', () => {
let stack = [];
stack.push(() => { throw new Error(); });
return compose(stack)({})
.then(() => {
throw new Error('promise was not rejected');
})
.catch(e => {
assert(e instanceof Error);
});
});
it('should work when yielding at the end of the stack with yield*', () => {
let stack = [];
stack.push(async(ctx, next) => {
await next;
});
return compose(stack)({});
});
it('should keep the context', () => {
const ctx = {};
const stack = [];
stack.push(async(ctx2, next) => {
await next();
assert.strictEqual(ctx2, ctx);
});
stack.push(async(ctx2, next) => {
await next();
assert.strictEqual(ctx2, ctx);
});
stack.push(async(ctx2, next) => {
await next();
assert.strictEqual(ctx2, ctx);
});
return compose(stack)(ctx);
});
it('should catch downstream errors', async() => {
const arr = [];
const stack = [];
stack.push(async(ctx, next) => {
arr.push(1);
try {
arr.push(6);
await next();
arr.push(7);
} catch (err) {
arr.push(2);
}
arr.push(3);
});
stack.push(async(ctx, next) => {
arr.push(4);
throw new Error();
});
await compose(stack)({});
assert.deepEqual(arr, [1, 6, 4, 2, 3]);
});
it('should compose w/ next', () => {
let called = false;
return compose([])({}, async() => {
called = true;
}).then(() => {
assert(called);
});
});
it('should handle errors in wrapped non-async functions', () => {
const stack = [];
stack.push(() => {
throw new Error();
});
return compose(stack)({}).then(() => {
throw new Error('promise was not rejected');
}).catch(e => {
assert(e instanceof Error);
});
});
// https://github.com/koajs/compose/pull/27#issuecomment-143109739
it('should compose w/ other compositions', () => {
let called = [];
return compose([
compose([
(ctx, next) => {
called.push(1);
return next();
},
(ctx, next) => {
called.push(2);
return next();
}
]),
(ctx, next) => {
called.push(3);
return next();
}
])({}).then(() => assert.deepEqual(called, [1, 2, 3]));
});
it('should throw if next() is called multiple times', () => {
return compose([
async(ctx, next) => {
await next();
await next();
}
])({}).then(() => {
throw new Error('boom');
}, err => {
assert(/multiple times/.test(err.message));
});
});
it('should return a valid middleware', () => {
let val = 0;
return compose([
compose([
(ctx, next) => {
val++;
return next();
},
(ctx, next) => {
val++;
return next();
}
]),
(ctx, next) => {
val++;
return next();
}
])({}).then(() => {
assert.strictEqual(val, 3);
});
});
it('should return last return value', () => {
const stack = [];
stack.push(async(context, next) => {
let val = await next();
assert.strictEqual(val, 2);
return 1;
});
stack.push(async(context, next) => {
const val = await next();
assert.strictEqual(val, 0);
return 2;
});
const next = () => 0;
return compose(stack)({}, next).then(val => {
assert.strictEqual(val, 1);
});
});
it('should not affect the original middleware array', () => {
const middleware = [];
const fn1 = (ctx, next) => {
return next();
};
middleware.push(fn1);
for (const fn of middleware) {
assert.equal(fn, fn1);
}
compose(middleware);
for (const fn of middleware) {
assert.equal(fn, fn1);
}
});
it('should not get stuck on the passed in next', () => {
const middleware = [(ctx, next) => {
ctx.middleware++;
return next();
}];
const ctx = {
middleware: 0,
next: 0
};
return compose(middleware)(ctx, (ctx, next) => {
ctx.next++;
return next();
}).then(() => {
assert.strictEqual(ctx.middleware, 1);
assert.strictEqual(ctx.next, 1);
});
});
});

43
test/utils/fastparse.js Normal file
View File

@ -0,0 +1,43 @@
const assert = require('assert');
const fastparse = require('../../lib/fastparse');
const URL_EMPTY_VALUE = null;
describe('fastparse(url)', () => {
it('should parse the requrst URL', () => {
let url = fastparse('/foo/bar');
assert.strictEqual(url.host, URL_EMPTY_VALUE);
assert.strictEqual(url.hostname, URL_EMPTY_VALUE);
assert.strictEqual(url.href, '/foo/bar');
assert.strictEqual(url.pathname, '/foo/bar');
assert.strictEqual(url.port, URL_EMPTY_VALUE);
assert.strictEqual(url.query, URL_EMPTY_VALUE);
assert.strictEqual(url.search, URL_EMPTY_VALUE);
});
it('should parse with query string', () => {
let url = fastparse('/foo/bar?fizz=buzz');
assert.strictEqual(url.host, URL_EMPTY_VALUE);
assert.strictEqual(url.hostname, URL_EMPTY_VALUE);
assert.strictEqual(url.href, '/foo/bar?fizz=buzz');
assert.strictEqual(url.pathname, '/foo/bar');
assert.strictEqual(url.port, URL_EMPTY_VALUE);
assert.strictEqual(url.query, 'fizz=buzz');
assert.strictEqual(url.search, '?fizz=buzz');
});
it('should parse a full URL', () => {
let url = fastparse('http://localhost:8888/foo/bar');
assert.strictEqual(url.host, 'localhost:8888');
assert.strictEqual(url.hostname, 'localhost');
assert.strictEqual(url.href, 'http://localhost:8888/foo/bar');
assert.strictEqual(url.pathname, '/foo/bar');
assert.strictEqual(url.port, '8888');
assert.strictEqual(url.query, URL_EMPTY_VALUE);
assert.strictEqual(url.search, URL_EMPTY_VALUE);
});
it('should not choke on auth-looking URL', () => {
assert.strictEqual(fastparse('//todo@txt').pathname, '//todo@txt');
});
});

265
test/utils/onfinish.js Normal file
View File

@ -0,0 +1,265 @@
const assert = require('assert');
const http = require('http');
const net = require('net');
const onFinished = require('../../lib/onfinish');
describe('onFinished(res, listener)', () => {
it('should invoke listener given an unknown object', done => {
onFinished({}, done);
});
describe('when the response finishes', () => {
it('should fire the callback', done => {
let server = http.createServer((req, res) => {
onFinished(res, done);
setTimeout(res.end.bind(res), 0);
});
sendGet(server);
});
it('should include the response object', done => {
let server = http.createServer((req, res) => {
onFinished(res, (err, msg) => {
assert.ok(!err);
assert.strictEqual(msg, res);
done();
});
setTimeout(res.end.bind(res), 0);
});
sendGet(server);
});
it('should fire when called after finish', done => {
let server = http.createServer((req, res) => {
onFinished(res, () => {
onFinished(res, done);
});
setTimeout(res.end.bind(res), 0);
});
sendGet(server);
});
});
describe('when using keep-alive', () => {
it('should fire for each response', done => {
let called = false;
let server = http.createServer((req, res) => {
onFinished(res, () => {
if (called) {
socket.end();
server.close();
done(called !== req ? null : new Error('fired twice on same req'));
return;
}
called = req;
writeRequest(socket);
});
res.end();
});
let socket;
server.listen(function(){
socket = net.connect(this.address().port, function(){
writeRequest(this);
});
});
});
});
describe('when requests pipelined', () => {
it('should fire for each request', done => {
let count = 0;
let responses = [];
let server = http.createServer((req, res) => {
responses.push(res);
onFinished(res, err => {
assert.ifError(err);
assert.strictEqual(responses[0], res);
responses.shift();
if (responses.length === 0) {
socket.end();
return;
}
responses[0].end('response b');
});
onFinished(req, err => {
assert.ifError(err);
if (++count !== 2) {
return;
}
assert.strictEqual(responses.length, 2);
responses[0].end('response a');
});
if (responses.length === 1) {
// second request
writeRequest(socket);
}
req.resume();
});
let socket;
server.listen(function(){
let data = '';
socket = net.connect(this.address().port, function(){
writeRequest(this);
});
socket.on('data', chunk => {
data += chunk.toString('binary');
});
socket.on('end', () => {
assert.ok(/response a/.test(data));
assert.ok(/response b/.test(data));
server.close(done);
});
});
});
});
describe('when response errors', () => {
it('should fire with error', done => {
let server = http.createServer((req, res) => {
onFinished(res, err => {
assert.ok(err);
server.close(done);
});
socket.on('error', noop);
socket.write('W');
});
let socket;
server.listen(function(){
socket = net.connect(this.address().port, function(){
writeRequest(this, true);
});
});
});
it('should include the response object', done => {
let server = http.createServer((req, res) => {
onFinished(res, (err, msg) => {
assert.ok(err);
assert.strictEqual(msg, res);
server.close(done);
});
socket.on('error', noop);
socket.write('W');
});
let socket;
server.listen(function(){
socket = net.connect(this.address().port, function(){
writeRequest(this, true);
});
});
});
});
describe('when the response aborts', () => {
it('should execute the callback', done => {
let client;
let server = http.createServer((req, res) => {
onFinished(res, close(server, done));
setTimeout(client.abort.bind(client), 0);
});
server.listen(function(){
let port = this.address().port;
client = http.get('http://127.0.0.1:' + port);
client.on('error', noop);
});
});
});
describe('when calling many times on same response', () => {
it('should not print warnings', done => {
let server = http.createServer((req, res) => {
let stderr = captureStderr(() => {
for (let i = 0; i < 400; i++) {
onFinished(res, noop);
}
});
onFinished(res, done);
assert.strictEqual(stderr, '');
res.end();
});
server.listen(function(){
let port = this.address().port;
http.get('http://127.0.0.1:' + port, res => {
res.resume();
res.on('end', server.close.bind(server));
});
});
});
});
});
/**********************************************************
* Removed request tests as those are not needed by our app
***********************************************************/
function captureStderr(fn){
let chunks = [];
let write = process.stderr.write;
process.stderr.write = function write(chunk, encoding){
chunks.push(new Buffer(chunk, encoding));
};
try {
fn();
} finally {
process.stderr.write = write;
}
return Buffer.concat(chunks).toString('utf8');
}
function close(server, callback){
return function(error){
server.close(err => {
callback(error || err);
});
};
}
function noop(){}
function sendGet(server){
server.listen(function onListening(){
let port = this.address().port;
http.get('http://127.0.0.1:' + port, res => {
res.resume();
res.on('end', server.close.bind(server));
});
});
}
function writeRequest(socket, chunked){
socket.write('GET / HTTP/1.1\r\n');
socket.write('Host: localhost\r\n');
socket.write('Connection: keep-alive\r\n');
if (chunked) {
socket.write('Transfer-Encoding: chunked\r\n');
}
socket.write('\r\n');
}