Compare commits

...

407 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
dead-horse 1015cea41d Release 2.8.0 2019-08-19 10:47:30 +08:00
Jake 5afff89eca feat: accept options in the Application constructor (#1372) 2019-08-19 10:44:09 +08:00
Gunnlaugur Thor Briem 3b23865340 docs: parameter of request.get is case-insensitive (#1373) 2019-08-19 10:43:35 +08:00
Jeff a245d18a13 docs: Update response.socket (#1357)
* docs: Include `dead-horse`'s comment 

https://github.com/koajs/koa/pull/1355#discussion_r302404837

* Update docs/api/response.md

Polish English

Co-Authored-By: Martin Iwanowski <martin@iwanowski.se>
2019-08-16 01:31:15 +08:00
Edvard Chen d1d65dd29d chore(deps): install egg-bin, mm as devDeps not deps (#1366) 2019-08-10 23:40:27 +08:00
Yiyu He 2c86b10fea
test: remove jest and use egg-bin(mocha) (#1363) 2019-07-30 18:00:43 +08:00
Peng Jie 219bf22237 docs(context): update link (#1354)
* docs(context): update link

* docs(response): add code highlight
2019-07-30 14:06:19 +08:00
Imon-Haque 52a673703a chore: ignore Intellij IDEA project files (#1361) 2019-07-24 19:06:56 +08:00
Jeff ff70bdc75a fix: typo on document (#1355) 2019-07-11 15:44:52 +08:00
Peng Jie b9e35469d3 docs(api): fix keygrip link (#1350) 2019-06-28 12:55:19 +08:00
dead-horse d4bdb5ed9e chore: update eslint and fix lint errors 2019-06-26 11:15:22 +08:00
dead-horse 12960c437c build: test on 8/10/12 2019-06-26 11:14:37 +08:00
Alex Berk 00e8f7a1b7 docs: ctx.type aliases ctx.response, not ctx.request (#1343) 2019-06-26 11:11:44 +08:00
Peng Jie 62f29eb0c4 docs(context): update cookies link (#1348) 2019-06-26 11:07:09 +08:00
Igor Adamenko b7fc526ea4 docs: fix typo in cookie path default value docs (#1340) 2019-06-10 14:49:53 +08:00
kzhang 23f7f545ab chore: simplify variable (#1332) 2019-06-03 16:04:01 +08:00
Dobes Vandermeer 132c9ee63f docs: Clarify the format of request.headers (#1325) 2019-06-03 15:58:02 +08:00
Andrew Peterson 5810f279a4 docs: Removed Document in Progress note in Koa vs Express (#1336)
closes #1335
2019-06-03 15:57:14 +08:00
Vern Brandl 75233d974a chore: Consider removing this return statement; it will be ignored. (#1322) 2019-04-17 11:33:36 +08:00
Vern Brandl 04e07fdc62 test: Buffer() is deprecated due to security and usability issues. so use the Buffer.alloc() instead (#1321) 2019-04-16 18:05:18 +08:00
rosald 130e363856 docs: use 'fs-extra' instead of 'fs-promise' (#1309) 2019-02-14 13:17:35 +08:00
James George 2f2078bf99 chore: Update PR-welcome badge url (#1299) 2019-02-02 09:44:11 +08:00
dead-horse 8b4e2cd3bc Release 2.7.0 2019-01-28 16:51:37 +08:00
Martin Iwanowski b7bfa7113b feat: change set status assert, allowing valid custom statuses (#1308) 2019-01-28 16:50:27 +08:00
James George 72f325b78e chore: add pr welcoming badge (#1291) 2019-01-15 00:35:24 +08:00
call me saisai b15115b2cb chore: Reduce unnecessary variable declarations (#1298) 2019-01-15 00:34:12 +08:00
dead-horse ad91ce2346 chore: license 2019 2019-01-07 16:41:45 +08:00
Francisco Ryan Tolmasky I b25e79dfb5 Mark two examples as live for the corresponding documentation change in https://github.com/koajs/koajs.com/pull/38. (#1031)
Reviewed by @tolmasky.
2019-01-06 01:51:55 -06:00
Mikhail Bodrov d9ef60398e chore: Optimize array split (#1295) 2019-01-03 20:48:31 +08:00
jeremiG 9be8583125 chore: replace ~~ with Math.trunc in res.length (option) (#1288) 2018-12-27 16:22:35 +08:00
James George 7e46c2058c docs: add link to the license file (#1290) 2018-12-27 16:21:54 +08:00
Douglas Wade 48993ade9b docs: Document other body types (#1285) 2018-12-12 12:33:36 +08:00
Douglas Wade acb388bc05 docs: Add security vulnerability disclosure instructions to the Readme (#1283) 2018-12-12 12:24:18 +08:00
Douglas Wade a007198fa2 docs: Document ctx.app.emit (#1284) 2018-12-07 15:28:23 +08:00
Douglas Wade f90e825da9 docs: response.set(fields) won't overwrites previous header fields(#1282) 2018-12-07 15:24:51 +08:00
Vikram Rangaraj fc93c05f68 docs: update readme to add babel 7 instructions (#1274) 2018-12-07 15:23:18 +08:00
Jordan 5560f72912 chore: use the ability of `content-type` lib directly (#1276) 2018-12-07 15:22:12 +08:00
dead-horse 281a04e8e1 Release 2.6.2 2018-11-10 01:43:06 +08:00
ZYSzys 325792aee9 docs: add table of contents for guide.md (#1267) 2018-11-10 01:42:08 +08:00
André Cruz 99051992a9 fix: Status message is not supported on HTTP/2 (#1264) 2018-11-10 01:41:21 +08:00
Martin Iwanowski 71aaa29591 docs: fix spelling in throw docs (#1269) 2018-11-05 17:16:27 +08:00
Jordan bc81ca9414 chore: use res instead of this.res (#1271) 2018-11-05 17:15:58 +08:00
Martin Iwanowski 0251b38a84 test: node v11 on travis (#1265) 2018-11-01 19:18:24 +08:00
Waleed Ashraf 88b92b4315 doc: updated docs for throw() to pass status as first param. (#1268) 2018-11-01 19:17:16 +08:00
dead-horse 6c0e0d6e29 Release 2.6.1 2018-10-23 15:21:02 +08:00
fengmk2 4964242834 fix: use X-Forwarded-Host first on app.proxy present (#1263) 2018-10-23 15:20:09 +08:00
fengmk2 e01cc5a1cf
Release 2.6.0 2018-10-23 13:22:45 +08:00
Martin Michaelis 9c5c58b183 feat: use :authority header of http2 requests as host (#1262)
close #1174
2018-10-23 13:21:20 +08:00
小雷 9146024e10 feat: response.attachment append a parameter: options from contentDisposition (#1240) 2018-10-08 22:43:11 +08:00
urugator d32623baa7 docs: Update error-handling.md (#1239)
Docs incorrectly states that exposed errors are being logged by default error listener:
"which simply log the error if error.expose is true"
https://github.com/koajs/koa/blob/master/lib/application.js#L185
2018-09-20 18:43:23 +08:00
dead-horse e6853af649 Release 2.5.3 2018-09-11 23:24:46 +08:00
fengmk2 2ee32f50b8 fix: pin debug@~3.1.0 avoid deprecated warnning (#1245)
closes https://github.com/koajs/koa/issues/1243
2018-09-11 23:22:22 +08:00
Clayton Ray 2180839eda docs: Update koa-vs-express.md (#1230) 2018-08-09 15:39:23 +08:00
dead-horse 41257aa91e Release 2.5.2 2018-07-12 14:16:53 +08:00
dead-horse 5fad7cd915 chore: update authors 2018-07-12 14:15:53 +08:00
dead-horse 311031421f deps: upgrade all dependencies 2018-07-12 14:14:25 +08:00
Yiyu He 0b930665a8
perf: avoid stringify when set header (#1220) 2018-07-12 14:10:12 +08:00
Yiyu He c6b8782553 perf: cache content type's result (#1218) 2018-07-11 17:49:25 +08:00
Yiyu He 162a5b3e78 perf: lazy init cookies and ip when first time use it (#1216) 2018-07-11 11:18:39 +08:00
小菜 74170caf0b chore: fix comment & approve cov (#1214) 2018-07-04 11:33:01 +08:00
Asiel Leal cde0bb10e9 docs: fix grammar 2018-07-01 01:30:22 +08:00
小菜 2cdbc52e38 test&cov: add test case (#1211) 2018-07-01 01:29:19 +08:00
initial-wu 02feadc4db Lazily initialize `request.accept` and delegate `context.accept` (#1209) 2018-06-25 16:13:20 +08:00
Ruben Bridgewater 8f047ddb84 fix: use non deprecated custom inspect (#1198)
Custom inspection with the `inspect` property is deprecated and will
not work in Node.js 11 anymore. This fixes it by using the custom
inspect symbol where existent and falls back to the old style in case
it does not exist.
2018-06-25 10:34:15 +08:00
initial-wu 77a4cfb829 Simplify processes in the getter `request.protocol` (#1203) 2018-06-07 22:50:26 +08:00
Jason Macgowan 4d42500e76 docs: better demonstrate middleware flow (#1195) 2018-06-06 14:52:20 +08:00
initial-wu ee1a933096 fix: Throw a TypeError instead of a AssertionError (#1199) 2018-06-06 12:55:20 +08:00
initial-wu ef33a79874 chore: mistake in a comment (#1201) 2018-06-04 10:18:42 +08:00
initial-wu 148f26f630 chore: use this.res.socket insteadof this.ctx.req.socket (#1177)
prefer a shorter path to the object
2018-04-27 23:51:47 +08:00
Grand 45903f228a chore: Using "listenerCount" instead of "listeners" (#1184) 2018-04-27 23:48:51 +08:00
dead-horse 45464b5174 Release 2.5.1 2018-04-27 01:10:57 +08:00
Martin Iwanowski 2ace7044ac test: node v10 on travis (#1182) 2018-04-25 16:21:57 +08:00
Ruben Bridgewater 13086d2fcd fix tests: remove unnecessary assert doesNotThrow and api calls (#1170)
* tests: fix error verification

So far the error message was not tested at all. This change makes
sure the error will actually be tested for.

* tests: remove unnecessary api calls

`assert.doesNotThrow` does not provide any benefit since it will
only catch errors and then rethrow in case of an error.
2018-04-09 19:36:52 -07:00
Shawn Cheung 8c17517809 use this.response insteadof this.ctx.response (#1163) 2018-03-23 17:56:32 +08:00
Martin Iwanowski 9cef2db87e deps: remove istanbul (#1151) 2018-02-19 03:34:38 -08:00
Anton Harniakou 0698d67fef Update guide.md (#1150) 2018-02-16 11:56:51 -08:00
dead-horse 916f914727 Release 2.5.0 2018-02-11 17:40:10 +08:00
Yiyu He 3c23aa5b74
feat: ignore set header/status when header sent (#1137) 2018-02-11 16:25:24 +08:00
Martin Iwanowski 0923ef6182 run coverage using --runInBand (#1141) 2018-02-09 11:13:45 +08:00
Joseph Lin e544012e9f [Update] license year to 2018 (#1130) 2018-02-06 14:01:36 -05:00
Paul Anderson a64e4ae982 docs: small grammatical fix in api docs index (#1111) 2018-01-30 21:05:49 -08:00
Tomas Ruud e5db6a9833 docs: fixed typo (#1112) 2018-01-30 21:05:19 -08:00
Mengdi Gao c2615ecc5c docs: capitalize K in word koa (#1126) 2018-01-30 21:05:00 -08:00
Alexsey 6baa41178d Error handling: on non-error throw try to stringify if error is an object (#1113) 2017-12-24 20:48:44 +08:00
Nick McCurdy 53bea20e79 Use eslint-config-koa (#1105) 2017-12-24 15:01:01 +08:00
Michał Gołębiowski-Owczarek 9068316fc7 Update mgol's name in AUTHORS, add .mailmap (#1100) 2017-12-24 14:55:45 +08:00
Nick McCurdy 79c3c73452 Avoid generating package locks instead of ignoring them (#1108) 2017-12-24 14:36:17 +08:00
Mars Wong 841844ee2f chore: update copyright year to 2017 (#1095) 2017-11-12 10:06:04 -06:00
jongleberry bd89dfcafc 2.4.1 2017-11-06 14:23:34 +00:00
jongleberry 418bb066af 2.4.0 – added missing 2.3.0 changelog 2017-11-06 14:16:32 +00:00
jongleberry c68a6966c2 2.3.0 2017-11-06 14:03:36 +00:00
jongleberry 687b73257b
travis: test node@9 2017-11-06 13:08:21 +00:00
Riceball LEE 53a4446123 expose the Application::handleRequest method (#950)
* * expose the Application::handleRequest method to extend the context

* * minor change the handleRequest comment
2017-11-06 12:21:52 +00:00
Martin Iwanowski 85ff544f7a deps: update min engines (#1040) 2017-11-06 12:18:58 +00:00
Bernie Stern 6029064756 HTTP/2 has no status message (#1048) (#1049) 2017-11-06 12:17:43 +00:00
Sergei Osipov 18e4fafb02 Update fresh to ^0.5.2 to close vulnerability (#1086) 2017-11-06 12:00:22 +00:00
Jonas Zhang e8a024cbc0 docs: ddd Chinese docs link for v2.x (#1092) 2017-11-03 11:13:19 +08:00
Taehwan, No 1e81ea3691 docs: update babel setup (#1077) 2017-10-18 03:22:26 -05:00
Nick McCurdy 43a1df8200 test: Remove --forceExit flag for Jest (#1071) 2017-09-26 21:58:37 -05:00
Pedro Pablo Aste Kompen 0168fd87a8 docs: Update middleware.gif (#1052) 2017-09-25 23:22:22 -05:00
Shawn Sit e1e030cc3d docs: command is wrong for running tests (#1065) 2017-09-25 23:08:21 -05:00
JamesWang 77ca4290a1 test: replace request(app.listen()) with request(app.callback()) 2017-09-25 23:07:57 -05:00
Martin Iwanowski 7f577aff2d meta: update AUTHORS (#1067) 2017-09-25 23:02:07 -05:00
Hrvoje Šimić f3ede44ffa docs: fix dead link to logo image (#1069) 2017-09-25 23:00:15 -05:00
Martin Iwanowski 7e78147f52 tools: remove --fix to catch lint errors (#1062) 2017-09-19 03:23:32 -05:00
Todor Stoychev 09f1b7bb1f docs: fix typo (#1058) 2017-09-13 12:58:43 -05:00
jongleberry 86ab4ae84a Revert "refactor: remove duplicate assignment in respond test (#1055)" (#1056)
This reverts commit c161c0f2e9.
2017-09-11 21:35:13 -05:00
Clark Du c161c0f2e9 refactor: remove duplicate assignment in respond test (#1055)
Signed-off-by: Clark Du <clark.duxin@gmail.com>
2017-09-11 10:22:34 -07:00
Usman Hussain 392e8aa90d Koa routing (third party libraries support) (#1038) 2017-08-17 10:20:57 -07:00
Yu Qi 78832ff6c6 bench: add bench for async/await (#1036) 2017-08-03 09:33:51 +08:00
Rico Sta. Cruz 7294eae078 koa-vs-express: Update note about generators (#1015) 2017-07-20 18:02:17 -07:00
Saad Quadri 40a6dd2c80 docs: improve consistency in api code examples (#1029) 2017-07-20 18:01:03 -07:00
Hartley Melamed 0c44c11ce3 Updates documentation to include reference for RFC2324 (HTTP Status 418) (#1028) 2017-07-19 11:04:59 -07:00
Luke Bousfield 67630217ae Fix context.inspect when called on the prototype (#1012)
* Fix context.inspect when called on the prototype

Fixes #837

* Add tests
2017-07-15 20:05:26 -04:00
haoxin 9f2182dec7 docs: fix typo (#1023) 2017-07-13 10:52:07 -07:00
Chiahao Lin 87cde82399 docs: modified examples using the wrong keyword (yield -> await) (#1021) 2017-07-07 00:48:31 -07:00
jongleberry aaac09af1a 2.3.0 2017-06-20 10:01:10 -07:00
Martin Iwanowski 327b65cb6b Use node 7+ WHATWG parser for hostname, fixes #1002 (#1004)
* Use node 7+ WHATWG parser for hostname, fixes #1002

* only use URL if host is IPv6, expose parsed URL

* catch invalid URLs, memoize empty obj

* hostname returns empty string when URL throws
2017-06-20 09:57:30 -07:00
Martin fl0w Iwanowski 012587889d added setters for header and headers, fixes #991 2017-06-20 09:57:07 -07:00
Martin fl0w Iwanowski f6f1ab73e1 lint: commit --fix 2017-06-20 09:57:07 -07:00
Martin fl0w Iwanowski 4b41a8b94b update gitignore, added package-lock 2017-06-20 09:57:07 -07:00
Yiyu He 41f8776350 test: run ci on node 7,8 (#992) 2017-06-20 09:56:47 -07:00
designgrill beec26ebf8 docs: A middleware is always responsible for calling downstream (#978)
Irrespective of whether it is called by an upstream middleware or not.
2017-06-12 11:38:25 -07:00
Kareem Kwong 0a7856ca15 docs: Add note about overwriting charset in response.type (#993) 2017-06-12 11:37:16 -07:00
Richard Marmorstein 7941fb5221 grammar (#994) 2017-06-12 11:36:19 -07:00
Equim 08eb1a20c3 docs: apply `Date.now()` to all docs (#988) 2017-05-21 23:33:25 +08:00
Shaun Warman 302814e7a3 docs: Fix minor nitpick in documentation (#987) 2017-05-21 13:40:08 +08:00
song bfce5806c2 Update Readme.md (#985)
Change ```new Date``` to ```Date.now```
2017-05-17 10:38:00 +08:00
Gilles De Mey d394724200 test: Use Jest (#981) 2017-05-11 11:30:32 +08:00
ziyunfei 13c7ca6139 res.type=: remove no-op code (#980) 2017-05-05 10:27:11 -07:00
joehecn 9248660efd test: fix spelling error (#972) 2017-04-29 10:30:24 +08:00
TJ Holowaychuk 32ebd1bd6a Update Readme.md
I'll fill everyone in a bit
2017-04-26 09:46:25 -07:00
Francisco Presencia 3bbb74b3ee docs: added note about arrays (#964)
Added note about arrays being returned as JSON.
2017-04-23 16:17:48 -07:00
bananaappletw 2da3dd49fa eslint: remove unused eslint-plugin-babel (#969) 2017-04-23 16:14:34 -07:00
Thiago Lagden ee5af59f1f replace apply by spread syntax (#971) 2017-04-23 16:14:16 -07:00
Remek Ambroziak 7f3d0765d7 docs: fix documentation to match new http-errors API (#957) (#967) 2017-04-21 21:49:06 +08:00
George Chung cd5d6a1c37 docs: reorganize "response.status=" section (#966)
Reference: https://github.com/jshttp/statuses/blob/v1.2.0/src/iana.json
Since koa@2.2.0 depends statuses@1.2.0
2017-04-21 15:50:39 +08:00
joehecn 1b3e08e046 test: change a describe to it (#963) 2017-04-20 10:43:06 +08:00
joehecn 19fc4194b7 Test: change a describe to it (#959) 2017-04-16 22:02:32 -06:00
Aesop Wolf cb12aa8ae5 readme: update the order of accepted functions(#958)
Update the order of accepted functions so that it matches the following blocks of text.
2017-04-14 11:50:31 -06:00
jongleberry 7bd82ac410 ⬆️ koa-compose@4 — remove any-promise 2017-04-12 22:47:17 -07:00
Martin Iwanowski 3721f6be0b test: remove redudant test case (#956) 2017-04-12 23:41:38 -06:00
bananaappletw efb05d2836 eslint: remove deprecated babel/arrow-parens (#953) 2017-03-28 00:26:09 -07:00
Ivan Kleshnin 87036857e6 readme: fix typo (#951) 2017-03-27 12:37:42 -07:00
Fangdun Cai 18d753ca2d use Buffer.from instead (#946) 2017-03-20 14:48:37 +08:00
Michał Gołębiowski 682d7b484a docs: add the missing next param (#942) 2017-03-20 01:02:21 +08:00
jongleberry 9a9949f9ee 2.2.0 2017-03-14 01:57:39 -07:00
Lee Bousfield 55d1d9a607 travis: cache node_modules (#934) 2017-03-14 01:55:54 -07:00
jongleberry 4816cd76f0 :arrow-up: deps
closes #939
2017-03-14 01:55:15 -07:00
jongleberry f841418b10 package: drop package.engines.node req to v6
closes #938
closes #936
closes #935
2017-03-14 01:48:36 -07:00
jongleberry ebed04f342 2.1.0 2017-03-07 23:09:56 -08:00
Lee Bousfield e6539e1cf2 Return middleware chain promise from `callback()` (#848)
The v2.x version of 8836cd3 on master.
2017-03-07 23:05:28 -08:00
jongleberry 65c130db5b readme docs++ (#932)
* Better expose documentation in overview readme

* Add code examples for context, request and response

* docs++

* fix indentation
2017-03-07 23:04:43 -08:00
jongleberry e812339033 docs: create v2 Migration document (#931)
* Give v2 migration documentation its own document. Incorporate docs from #533

* Fix mis-capitalization of Koa

* Remove unnecessary Dependency section

* Hint at koa-convert enabled compatibility

* Add section on constructing with new

* Clarify es6 constructors are used

* Fix varying capitalization

* Restore mistakenly removed Dependency changes section

* v1.x should not receive feature updates

* Add next() to signature, add missing backticks

* docs++
2017-03-07 22:59:42 -08:00
jongleberry e9d7abaf79 res: use http.ServerResponse._header when accessors exist (#930)
* Don't use http.ServerResponse._header when accessors exist

Structure of http.ServerResponse._header will change in future
Node versions. Avoid reading and setting it directly when
helpers exist.

* Add new header test case

* make things a little more strict
2017-03-07 22:59:24 -08:00
Jeff Moore 188c0968e1 Add Troubleshooting documentation (#921)
* Move Common Problems wiki documentation to troubleshooting.md

* Fix header levels and TOC linking

* Change header structure, move causes to their own sections

* Fix fragment capitalization

* Add an example for missing await

* Cleanup language due to problem generalization

* Add visibility for troubleshooting

* Fix fragment typo
2017-03-01 15:50:02 -08:00
fengmk2 e452b68bd9 feat: set err.headerSent before app error event emit (#919) 2017-02-28 10:52:54 +08:00
Ilkka Oksanen 0c28d1f57b Fix async/await babel recommendation 2017-02-26 12:23:42 -06:00
Stéphane Bisinger 909b829ac6 Fix typo 2017-02-26 12:23:11 -06:00
Ilkka Oksanen d740d9b2b1 Update the link to migration instructions (#916) 2017-02-26 21:41:14 +08:00
Yiyu He 73e9e13580 chore: unify badge's style (#914) 2017-02-26 15:57:49 +08:00
jongleberry 612a77b72b travis: test node@8 nightly 2017-02-25 14:09:51 -06:00
石发磊 207aa4e75b readme: change `generatorFunction` to `generator function` (#912)
change word `generatorFunction` to `generator function`, because to correspondence with the above form
2017-02-25 12:59:55 -06:00
Filip Skokan 0c2d881b67 update engines in package.json for 7.6.0 min node (#911) 2017-02-25 21:29:16 +08:00
Xavier Damman 2fda9dd0bf Added OpenCollective backers and sponsors (#748)
* Added OpenCollective backers and sponsors

* Removed become a backer/sponsor links
2017-02-25 00:50:07 -06:00
jongleberry 6c6aa4dab4 2.0.1 2017-02-25 00:47:45 -06:00
jongleberry 4e5deaeb9c history++ 2017-02-25 00:38:20 -06:00
jongleberry 65061085e4 package: remove publish tag 2017-02-25 00:30:37 -06:00
jongleberry d4b32234ea docs++ 2017-02-25 00:30:11 -06:00
jongleberry f191c0def4 package: oops re-add babel-eslint 2017-02-25 00:23:31 -06:00
fengmk2 a7c4236728 fix: add named arrow function for request and response handlers (#805)
cherry-pick #804
2017-02-25 00:06:41 -06:00
jongleberry cd02834f75 travis: only test node v7.6.0+ 2017-02-25 00:06:01 -06:00
jongleberry 9671add57d test: remove babel tests as they are no longer needed in node v7.6 2017-02-25 00:05:25 -06:00
jongleberry df7b4ff2b8 docs: replace the reference to co
closes #893
2017-02-24 23:43:26 -06:00
Amit Portnoy 00156dfec6 remove unused app.name (#899) 2017-02-18 14:01:58 -08:00
Lee Bousfield 152b6d73b7 Fix response.status default value documentation (#889)
Resolves #888, also see #705
2017-02-18 13:57:45 -08:00
Wang Dàpéng 8435268efc Upgrade mocha (#900) 2017-02-13 23:02:58 +08:00
dead-horse d48291f40a Release 2.0.0-alpha.8 2017-02-13 11:10:34 +08:00
Rui Marinho 7ae9c3e109 Fix malformed content-type header causing exception on charset get (#898) 2017-02-13 11:05:35 +08:00
iamchenxin 2db3b1b49a Fix typo for accepts(). (#863)
it return {String|Array|false}, never return undeifined.
2016-12-07 00:22:11 +08:00
iamchenxin fabf5864c6 Amend typo, request.is() return null|fasle|string. (#864)
Modifying the test for `null` from `==` to `===` to make sure it must be `null`.
2016-12-01 18:16:02 +08:00
Malcolm e3ccdac621 Clarify behavior of ctx.throw in docs (#856) 2016-11-25 11:51:02 +08:00
Avindra Goolcharan 2a16426afe nit: fix grammar in generator deprecation warning (#834)
This fixes the tense of `will been` to `will be`
2016-10-17 17:45:06 +02:00
dead_horse ce78786f95 Release 2.0.0-alpha.7 2016-09-07 16:29:46 +08:00
Adam Lau 21c0d823dd fix: subdomains should be [] if the host is an ip (#808)
Closes: #775
2016-09-07 16:21:32 +08:00
dead_horse e4c0a53421 Release 2.0.0-alpha.6 2016-08-29 11:25:30 +08:00
Yiyu He 4338cb6c14 [breaking change] don't bind onerror to context (#800) 2016-08-29 11:18:30 +08:00
Ivan Lyons 614b4e1ad7 `yield` => `await` (#798) 2016-08-21 21:21:09 -07:00
jongleberry 0d7aeb1f7c 2.0.0-alpha.5 2016-08-10 12:16:39 -07:00
jongleberry 2abed6ec75 fix: res.flushHeaders() (#795)
* fix: res.flushHeaders()

* remove arg to flush headers

* fix tests for node v4 and v5
2016-08-10 12:15:48 -07:00
Jacob Bass ce75a9c872 fixup babel setup instructions (#794)
old instructions pointed to incorrect npm module names
2016-08-06 07:06:31 +02:00
dead_horse 742a675e60 Release 2.0.0-beta.4 2016-07-24 02:06:19 +08:00
dead_horse a1cdbdafcf docs: update babel setup
closes #783
2016-07-24 01:50:17 +08:00
Yiyu He 23903e7ef4 fix(response): correct response.writable logic (#782) 2016-07-24 01:20:29 +08:00
qingming 2c507d335d fix typo (#756) 2016-06-21 20:01:51 -07:00
Martin Iwanowski d47d0f9619 fix broken link, fixes #741 (#745) 2016-05-27 07:41:22 +02:00
Yu Qi b16d24ecb8 add maxAge and overwrite options to ctx.cookies.set(name, value, [options]). (#742) 2016-05-26 13:31:11 +08:00
jongleberry e61d54565b docs: add more docs about app.context
references:

- https://github.com/koajs/koa/issues/652
2016-05-14 08:36:50 -07:00
Zack Tanner 808768a597 copy tweaks (#731) 2016-05-13 14:27:18 +08:00
jongleberry 41afac3eba travis: test node@6 2016-05-01 13:20:39 -07:00
Yu Qi c979056087 fix tests on node 6
parsed querystrings no longer inherit from the Object prototype
2016-04-30 09:19:29 +02:00
d3v e0a0d9f7cc fix: v1 artifact "this" reference (#711) 2016-04-20 14:15:11 +08:00
PlasmaPower 54e58d3523 req: Cache the request IP 2016-04-03 19:30:06 -07:00
PlasmaPower 826ad83db6 travis: move to container travis builds 2016-04-03 19:28:06 -07:00
Marceli.no fd839976e2 Typo
Significant, but insignificant. Feel free to discard this pull request and make the change yourselves :)
2016-04-03 08:41:04 +08:00
jongleberry be87ef8a24 lint: upgrade packages, fix generator star spacing 2016-03-28 14:21:05 -07:00
Yiyu He 177b599fd5 Merge pull request #699 from PlasmaPower/patch-2
Correct ctx.assert documentation parameter order (v2.x)
2016-03-28 01:25:09 +08:00
PlasmaPower 9c6969755f Correct ctx.assert documentation parameter order 2016-03-26 21:26:09 -06:00
jongleberry bd3d9e5100 Merge pull request #690 from PlasmaPower/patch-1
Docs: v2 error handling is environment independent
2016-03-22 11:50:11 -07:00
PlasmaPower efcdd3b93e Docs: v2 error handling is environment independent 2016-03-22 12:22:43 -06:00
jongleberry 2b094eb895 docs: update history and docs for v2 2016-03-22 11:03:19 -07:00
jongleberry daf688bf69 Merge pull request #683 from fl0w/v2.x
convert generator-mw with deprecation warning
2016-03-22 10:13:35 -07:00
Martin Iwanowski 0ac4ff00c6 Convert generator-mw with deprecation warning 2016-03-22 07:35:05 +01:00
jongleberry a1aec3d163 Merge pull request #686 from robinpokorny/lint-js-in-markdown
Correct code style of JavaScript in Markdown
2016-03-21 12:22:44 -07:00
Robin Pokorný 340dd4f1a3 Lint JavaScript in Markdown 2016-03-16 16:50:10 +01:00
dead_horse 39f058e11c fix cookies' secure detect 2016-03-15 13:57:22 -07:00
jongleberry bcada5bde9 readme: update URLs based on HTTP redirects
see: a25ab12116
2016-03-15 12:38:44 -07:00
Yiyu He 9ba2f9c5df Merge pull request #681 from PlasmaPower/update-error-handling-doc
Update error handling doc to use promises+async (v2.x)
2016-03-14 23:16:01 +08:00
Lee Bousfield 1d1698eb85 Update error handling doc to use promises+async 2016-03-12 20:19:55 -07:00
jongleberry 882ea7e87a Merge pull request #678 from PlasmaPower/cherrypick-to-next
Add support for headers in errors (v2.x)
2016-03-12 18:33:47 -08:00
Jonathan Ong 04a7122e22 lint: benchmarks/ 2016-03-12 18:33:02 -08:00
jongleberry 2df468b524 Merge pull request #679 from PlasmaPower/remove-unused-http
Remove unused http variable (v2.x)
2016-03-12 18:32:39 -08:00
Jonathan Ong 551715821b travis: run lint 2016-03-12 18:30:43 -08:00
Lee Bousfield 597638ded6 Remove unused http variable
ESLint now runs fine
2016-03-12 17:53:14 -07:00
Lee Bousfield a440425dc2 Add support for headers in errors 2016-03-12 17:46:35 -07:00
Bartol Karuza 3d15c2409d JSDoc question/suggestion on optional parameters
Hi, Webstorm keeps giving me warnings on the 'redirect' method, because the JSDoc specified two input parameters, both required. There is a JSDoc standard for optional parameters. What is your view on using these in KOA documentation?
http://usejsdoc.org/tags-param.html#optional-parameters-and-default-values

closes #661
2016-03-12 14:22:42 -08:00
Jonathan Ong d768ed83b6 docs: note stream error handling and destruction. ref: #612 2016-03-12 14:22:28 -08:00
TJ Holowaychuk 86a6f2b380 add CODE_OF_CONDUCT.md
aka dont be an asshole
2016-03-12 14:22:16 -08:00
dead_horse a808671340 add app.silent, err.status, err.expose to doc, fixes #630 2016-03-12 14:20:46 -08:00
Prayag Verma 53a165f543 chore(license): update license year to 2016 2016-03-12 14:19:44 -08:00
Louis DeScioli d74802dc70 Standardizes instances of removeHeader to remove 2016-03-12 14:19:27 -08:00
jongleberry 7373c7eca1 comments: remove vague TODOs
closes #576
2016-03-12 14:11:19 -08:00
Jonathan Ong 82bdb8223d travis: install wrk 2016-03-12 14:10:02 -08:00
Jonathan Ong 5d8b759e11 travis: run 'make bench'
closes #191
2016-03-12 14:09:33 -08:00
jongleberry 5d330b095f use codecov instead of coveralls 2016-03-12 14:09:06 -08:00
Jonathan Ong dbcbc28b11 package: ⬆️ dependencies 2016-03-12 14:03:10 -08:00
jongleberry b658fe7ca0 Merge pull request #675 from PlasmaPower/flush-headers
Add support for flushing headers
2016-03-12 13:49:02 -08:00
Lee Bousfield 6a147726bd Add support for flushing headers 2016-03-03 21:01:56 -07:00
Yiyu He 291c7c329f Merge pull request #651 from geekplux/patch-1
fix the error message
2016-01-24 13:40:42 +08:00
Xiang Gao 897ad7aca8 fix the error message 2016-01-24 12:35:03 +08:00
jongleberry 5384f10497 Merge pull request #637 from koajs/Pana-v2.x
Pana v2.x
2016-01-17 16:06:31 -08:00
pana 1e38b13a94 docs: update docs for koa v2
update readme and request, response toJSON method

update readme

update readme

update readme

update readme

update readme

update readme

update docs

update doc

pretty readme

update docs

fix then callback
2016-01-17 16:05:49 -08:00
Yanick Rochon d134fff9e8 Fix issue when app.use() is called with empty value 2015-11-25 12:10:55 +08:00
dead_horse 69ad7ad4a5 publishConfig: update next tag 2015-11-24 15:55:51 +08:00
Yiyu He 6a5c2e529a Merge pull request #595 from nvartolomei/master
Fix param tag on Application.use method
2015-11-14 22:57:45 +08:00
Nicolae Vartolomei 61f7c6b5c5 Fix param tag on Application.use method 2015-11-14 15:46:29 +02:00
Yiyu He 667628d333 Merge pull request #592 from nswbmw/master
update examples in readme
2015-11-08 23:13:14 +08:00
nswbmw aac3d70895 update readme 2015-11-08 13:26:50 +08:00
fengmk2 da6c63da59 Merge pull request #588 from koajs/539-babel-test
test: add a babel example
2015-11-07 12:12:40 +08:00
jongleberry a09000357d add docs for babel 2015-11-06 09:45:53 -08:00
jongleberry 65f645d341 use babel-plugin-transform-async-to-generator 2015-11-06 09:40:44 -08:00
jongleberry eb0bd4c2c3 test: fix use-strict typo 2015-11-06 09:38:00 -08:00
jongleberry 51b51331ba us eslint-plugin-babel for better listing 2015-11-06 09:37:35 -08:00
jongleberry a6547bcbce ⬆️ babel and use async arrow functions 2015-11-05 08:49:20 -08:00
jongleberry 09ada29881 test: add a babel example 2015-11-05 08:47:08 -08:00
dead_horse 848a9c885b ocd 2015-11-06 00:42:14 +08:00
Yiyu He 6e76e20f43 Merge pull request #589 from frankxin/patch-1
correct a spelling error : appropriately
2015-11-05 10:50:07 +08:00
dead_horse 34e8325a39 Release 2.0.0-alpha.3 2015-11-05 10:41:00 +08:00
Yiyu He b83405f052 Merge pull request #586 from koajs/url
ensure parseurl always working as expected
2015-11-05 10:37:11 +08:00
frank 4b5ef85f29 correct a spelling error : appropriately 2015-11-05 10:13:07 +08:00
dead_horse 08057e386a ensure parseurl always working as expected 2015-11-04 15:08:49 +08:00
jongleberry ad6f752cff eslint: add prefer-arrow-callback 2015-11-02 11:25:12 -08:00
broucz 4b1a1da652 test: switch all functions to arrow functions
closes #553

Update test: application -> use() should throw if not a function

Fix lint

Use arrow function

Refactor test using arrow function

Remove non mandatory brackets

fix for merge

Fix: missing refactor after merge

Use arrow function for old generator
2015-11-02 11:22:05 -08:00
TJ Holowaychuk 5bfe0d4081 Merge pull request #579 from stojanovic/fix/todo-headers
Remove 'TODO' comment for this.res._headers
2015-10-31 12:23:38 -07:00
Slobodan Stojanovic 0470997854 Remove 'TODO' comment for this.res._headers
Node probably doesn't plan to change this so there's no point of having 'TODO' comment in the code.
2015-10-31 19:21:40 +01:00
TJ Holowaychuk 439f051776 Merge pull request #570 from koajs/fix-app-inspect
fix Application.inspect() – missing .proxy value. Closes #563
2015-10-31 11:15:09 -07:00
TJ Holowaychuk 308ceee47d Merge pull request #574 from stojanovic/fix/test-todo
Remove TODO from the response type test
2015-10-31 06:10:16 -07:00
Slobodan Stojanovic 275356a5ce Remove TODO from the response type test 2015-10-31 14:01:26 +01:00
blaz 3560651bbc Add usage of koa-convert for legacy middleware
closes #565
closes #538
2015-10-30 20:20:00 -07:00
Julian Gruber eb03d9f61e Merge pull request #573 from stojanovic/feat/node-5
Add node 5 to travis
2015-10-30 10:04:49 +01:00
Slobodan Stojanovic a135d932a0 Add node 5 to travis 2015-10-30 07:34:23 +01:00
TJ Holowaychuk aa1fbbff4a fix Application.inspect() – missing .proxy value. Closes #563
fix trailing comma
2015-10-29 09:56:15 -07:00
Slobodan Stojanovic b08facb7bd Fix indentation and add .eslint rules
closes #555
2015-10-29 09:55:34 -07:00
Slobodan Stojanovic 6d3b81fe50 Update benchmark, remove --harmony flag and use arrow functions
closes #567

Fix benchmark middleware - in promises body is on ctx not on this
2015-10-28 14:26:04 -07:00
Tejas Manohar ae9edb6dc9 docs: babel-node required -> Babel
closes #562
2015-10-28 12:31:10 -07:00
jongleberry 664161a227 2.0.0-alpha.2 2015-10-27 16:31:24 -07:00
Felix Becker ebb4850709 Remove co dependency
closes #558
closes #557

Change tests to use plain functions and promises

Add test

return promise in middleware

Change benchmarks to use plain functions and promises

typeerror
2015-10-27 16:24:25 -07:00
Tejas Manohar ded7a17140 deprecate env-specific logging in v2
closes #561
2015-10-27 16:21:19 -07:00
jongleberry 72680825d0 Merge pull request #542 from menems/master
Add 2.0.0 Examples #538
2015-10-27 16:11:33 -07:00
blaz d280122cf4 Add 2.0.0 Examples 2015-10-28 00:02:27 +01:00
Slobodan Stojanovic 5d4df1086d Add arrow functions rules to .eslint file
closes #549
2015-10-25 13:26:57 -07:00
Slobodan Stojanovic cc1d41f5e3 Refactor tests - add arrow functions
Refactor tests - move .should to the same line as arrow function
2015-10-25 13:26:36 -07:00
broucz e859c602d1 Update test: application -> use() should throw if not a function
closes #550

Fix lint

Use arrow function

Remove not necessary import
2015-10-25 12:53:05 -07:00
fundon db7da4eb4f update bench case
closes #544
2015-10-25 12:48:26 -07:00
TJ Holowaychuk 0cbe1cab6f Merge pull request #547 from stojanovic/master
Refactor application.js - use arrow function in callback and remove EventEmitter from 'event' module
2015-10-24 17:49:32 -07:00
Julian Gruber 0363608eba Merge pull request #548 from HarleyKwyn/patch-1
Fix typo in example for Koa instantiation
2015-10-24 21:37:31 +02:00
Kwyn Alice Meagher dce805b0ad Fix typo in example for Koa instantiation
need a capital Koa constant
2015-10-24 12:35:08 -07:00
Slobodan Stojanovic dac250b3af Refactor - EventEmitter is already exported by 'events' module
The 'events' module already exports 'EventEmitter' constructor function - https://github.com/nodejs/node/pull/2921
2015-10-24 17:30:25 +02:00
Slobodan Stojanovic 0df400fa60 Refactor application.js - use arrow function in callback 2015-10-24 16:19:56 +02:00
Julian Gruber c86e3faeb1 Merge pull request #546 from stojanovic/master
Update docs - add template strings and arrow functions where applicable
2015-10-24 14:18:08 +02:00
Slobodan Stojanovic 863fce310b Update docs - add template strings and arrow functions where applicable 2015-10-24 14:01:53 +02:00
Julian Gruber 95a5bcd3e0 Merge pull request #545 from Pana/master
Update docs
2015-10-24 11:52:56 +02:00
pana b6b0d02df9 update docs 2015-10-24 17:21:47 +08:00
jongleberry 773aed6622 history: add 1.1.1 2015-10-22 16:40:55 -07:00
jongleberry c2206a287d 2.0.0-alpha.1 2015-10-22 16:37:49 -07:00
jongleberry 2e8cdab8bc support async functions
closes #530
closes #415
2015-10-22 16:34:59 -07:00
jongleberry 16db0f60c4 eslint: add no-var rule 2015-10-22 15:46:47 -07:00
jongleberry e7c72d7873 ⬆️ istanbul@0.4 2015-10-22 15:43:15 -07:00
Santiago Sotomayor 0c438ed435 unset content-type when the type is unknown
closes #532
closes #536
2015-10-22 15:39:16 -07:00
TJ Holowaychuk 653d7ed26d Merge pull request #529 from aquascaper/api-index-typo
Fixed typo in docs/api/index.md
2015-10-13 13:21:08 -07:00
Arjun 3045b283fd Fixed typo in docs/api/index.md 2015-10-13 16:09:01 -04:00
jongleberry e6d76da1e5 Merge pull request #525 from targos/eslint
eslint
2015-10-13 10:18:05 -07:00
Michaël Zasso 0a2d0dad75 add editorconfig 2015-10-13 09:23:57 +02:00
Michaël Zasso b5c09a1719 test: fix style issues 2015-10-13 09:23:57 +02:00
Michaël Zasso a157937969 lib: fix style issues 2015-10-13 09:23:57 +02:00
Michaël Zasso 24ccde947d add eslint and standard config 2015-10-13 09:23:06 +02:00
TJ Holowaychuk bfa53fbc28 Merge pull request #528 from tejasmanohar/rid_of_instanceof_hack
get rid of instanceof hack in application constructor
2015-10-13 00:21:42 -07:00
Tejas Manohar 132b32b287 get rid of instanceof hack in application constructor 2015-10-13 02:17:47 -05:00
TJ Holowaychuk c06d30286f Merge pull request #527 from tejasmanohar/module_exports
no more exports! only module.exports
2015-10-12 23:46:56 -07:00
TJ Holowaychuk 13983ede63 Merge pull request #526 from tejasmanohar/es2015_classes
refactor Application into an es6 class
2015-10-12 23:45:27 -07:00
Tejas Manohar 46f8d49e4c no more exports! only module.exports 2015-10-13 01:43:23 -05:00
Tejas Manohar 93ade5e2dd refactor Application into a class 2015-10-13 01:19:42 -05:00
Tejas Manohar e8f79d43f9 modularize tests for application
closes #517

add index test for Application

add app.toJSON test

add test for app.inspect()

add tests for app.use()

add tests for app.onerror()

add tests for app.respond()

add tests for app.context()

add tests for app.request()

add tests for app.response

refactor for non-existence of test/app...js

no need for *.js

use helpers/ dir for non-tests
2015-10-12 00:08:06 -07:00
Jonathan Ong ef467caabd readme: link to AUTHORS file 2015-10-12 00:04:58 -07:00
Jonathan Ong 27e8edc509 update AUTHORS 2015-10-12 00:04:06 -07:00
Robert Sköld e900f0a44a Use shorthand functions
closes #519
2015-10-12 00:00:41 -07:00
Tejas Manohar a27781abb5 add myself (tejasmanohar) to the authors list
closes #518
2015-10-11 23:59:05 -07:00
Jonathan Ong 00b3c97258 dev: use istanbul instead of istanbul-harmony
seems to work now…
2015-10-11 21:57:03 -07:00
Tejas Manohar 1a55281fee fix benchmarks- cant use const there 2015-10-11 21:22:33 -07:00
Tejas Manohar a13ae6fc95 use template strings in readme es6 2015-10-11 21:22:33 -07:00
Tejas Manohar 88c35c1a0e update minimum node version in README 2015-10-11 21:22:33 -07:00
Tejas Manohar 91ecce1d76 use arrow fn to avoid var self = this 2015-10-11 21:22:33 -07:00
Tejas Manohar ed19e67055 refactor to use ES6 template strings
replace string interp w/ templates in core

use string templating es6 in benchmarks

template strings in tests dir
2015-10-11 21:22:33 -07:00
Tejas Manohar 9f27c1c414 refactor to use ES6 const
change var to const for static require()'d modules

make constant var references in app use const keyword

refactor context to use es6 constants

refactor request to use es6 constants, let block-scope coming next

use const in response object for static refs

make context tests use es6 constants

experimental unit tests -> const

use const for static references in unit test over req

use const for static refs in res tests

update app tests to use const for static refs

make the context test use es6 constants for static refs

use constants in the README
es6 constants seem to work in --harmony on 0.12 too

use const's for immutable refs in benchmarks

ensure all JS files have blank newline at top

add newline to bottom of file where missing

add a webchat freenode link to irc channel

no need to assign error in catch{}-able test

app.silent option to turn off err logging

keep test env logging for backwards-compat
2015-10-11 21:22:33 -07:00
Jonathan Ong 07619f65c3 remove support for node < 4 2015-10-11 21:22:33 -07:00
Tejas Manohar 96c1e0998f don't use 'exports', only 'module.exports'
closes #513
2015-10-11 21:18:32 -07:00
Jonathan Ong af0ae08dc4 1.1.0 2015-10-11 16:31:05 -07:00
Tejas Manohar 0b1b49cb8a use strict in all .js files
closes #508
2015-10-11 16:08:32 -07:00
fengmk2 f875eb0c30 Merge pull request #486 from tejasmanohar/app_silent
app.silent option to turn off err logging
2015-10-09 11:22:58 +08:00
Tejas Manohar 6c19c41c09 keep test env logging for backwards-compat 2015-10-08 19:02:36 -05:00
TJ Holowaychuk 65cc864c9b Merge pull request #491 from tejasmanohar/remove_err_assignment
no need to assign error in catch{}-able test
2015-10-08 14:39:15 -07:00
Tejas Manohar e717733aa8 no need to assign error in catch{}-able test 2015-10-06 19:43:09 -05:00
TJ Holowaychuk 237e6c4f60 Merge pull request #490 from tejasmanohar/webchat_irc_link
add a webchat freenode link to irc channel
2015-10-06 09:22:19 -07:00
TJ Holowaychuk 65b59a9509 Merge pull request #489 from tejasmanohar/blank_newline_at_top
ensure all JS files have blank newline at top/bot
2015-10-06 09:21:40 -07:00
Tejas Manohar 10f9811e37 add a webchat freenode link to irc channel 2015-10-05 18:22:56 -05:00
Tejas Manohar 5e21238594 add newline to bottom of file where missing 2015-10-05 18:19:32 -05:00
Tejas Manohar ea4754e332 ensure all JS files have blank newline at top 2015-10-05 18:18:03 -05:00
Tejas Manohar c369b33b23 app.silent option to turn off err logging 2015-10-05 17:51:26 -05:00
dead_horse b2bcbcec7c docs: add request.origin 2015-10-03 11:38:48 +08:00
TJ Holowaychuk 1ed691dde3 fix some test formatting 2015-10-02 18:49:20 -07:00
TJ Holowaychuk 2359b6a769 Merge pull request #483 from squarejaw/test-coverage
Increase test coverage
2015-10-02 18:44:49 -07:00
Bryan Bess 0192d21d73 Increase test coverage 2015-10-02 19:18:05 -05:00
fengmk2 039217ad4e Merge pull request #481 from squarejaw/typo
Fix typo in test
2015-09-26 22:46:23 +08:00
Bryan Bess 890244fc74 Fix typo 2015-09-26 09:23:17 -05:00
Yiyu He b3d46bd69e Merge pull request #480 from chentsulin/ctx.origin
implement ctx.origin
2015-09-21 22:23:44 +08:00
C.T. Lin 85860587cc implement ctx.origin 2015-09-20 23:49:37 +08:00
mdemo 520163fe57 add node 4.x on travis ci 2015-09-10 01:36:38 +08:00
Jonathan Ong 9bf6bf1c07 improve verbage based on comments 2015-08-30 22:07:54 -07:00
llambda 30c8723705 Update Readme.md 2015-08-30 22:05:33 -07:00
fengmk2 f530219862 Merge pull request #475 from koajs/app-context-docs
Add docs for app.context
2015-08-31 12:06:53 +08:00
Travis Jeffery e710b4b05f add app.context docs 2015-08-30 22:20:53 -05:00
dead_horse 6563e6ac3d get rid of 0.11 2015-08-30 23:42:44 +08:00
dead_horse 36a933375b fix comment 2015-08-25 16:49:05 +08:00
gyson 1be333ca31 change respond() to a regular function
remove `yield* next` in lib/application, which caused annoy `A promise
was converted into a generator …` message.

benchmark result:
* when bench with native Promise, it has no impact for both stable and
experimental ones.
* when bench with Bluebird, it’s about 5-10% faster than original for
both stable and experimental ones.

closes #472
2015-08-23 13:55:20 -07:00
Jonathan Ong 0b9c032af1 1.0.0 2015-08-22 14:47:31 -07:00
AlexeyKhristov 8804b7ba6f add this.req check for querystring() 2015-08-22 14:40:36 -07:00
Sterling Williams 391650518f Do not log on expected http errors 2015-08-22 14:39:38 -07:00
dead_horse e021a6e7cb update authors 2015-08-20 01:05:43 +08:00
dead_horse d3ca581ac9 build: support iojs 3.x 2015-08-20 01:01:27 +08:00
jongleberry 69ed37335b Merge pull request #460 from mking/patch-1
Clarify precondition for freshness check
2015-07-16 07:27:48 -07:00
Matthew King 05b5912912 Clarify precondition for freshness check
I ran into the issue from #294 and thought it was a bug since fresh was always returning false. I think it's fair that, at the very least, the docs are not completely clear on how to use `this.fresh`.
2015-07-16 00:37:41 -07:00
Julian Gruber a8689e4e41 Merge pull request #457 from th507/master
Fixed a possible typo in the test
2015-07-03 13:01:04 +02:00
Jingwei "John" Liu b80007c460 fix a possible typo 2015-07-03 18:55:24 +08:00
Yiyu He 0ad06c9810 Merge pull request #446 from targos/upgrade-should
deps: upgrade should, install should-http
2015-05-24 20:28:40 +08:00
Michaël Zasso 0618db83c1 deps: upgrade should, install should-http 2015-05-24 11:08:20 +02:00
Yiyu He 2c9f2dcd9c Merge pull request #445 from yorkie/fix/gh-444
upgrade supertest to ^1.0.1
2015-05-24 14:19:58 +08:00
Yazhong Liu 90b05c09e5 upgrade supertest to ^1.0.1 2015-05-24 12:39:53 +08:00
120 changed files with 8387 additions and 4125 deletions

16
.editorconfig Normal file
View File

@ -0,0 +1,16 @@
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab

3
.eslintrc.yml Normal file
View File

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

2
.gitignore vendored
View File

@ -2,3 +2,5 @@ node_modules
test.js
coverage
npm-debug.log
.idea
*.iml

1
.mailmap Normal file
View File

@ -0,0 +1 @@
Michał Gołębiowski-Owczarek <m.goleb@gmail.com>

1
.npmrc Normal file
View File

@ -0,0 +1 @@
package-lock=false

View File

@ -1,8 +1,21 @@
sudo: false
language: node_js
node_js:
- "0.12"
- "iojs-v1"
- "iojs-v2"
sudo: false
script: "make test-travis"
after_script: "npm install coveralls@2 && cat ./coverage/lcov.info | coveralls"
- 8
- 10
- 12
cache:
directories:
- wrk/bin
- node_modules
before_script:
- npm prune
- "[ ! -f wrk/bin/wrk ] && rm -rf wrk && git clone https://github.com/wg/wrk.git && make -C wrk && mkdir wrk/bin && mv wrk/wrk wrk/bin || true"
- export PATH=$PATH:$PWD/wrk/bin/
script:
- npm run lint
- npm run test-cov
- npm run bench
after_script:
- npm install codecov
- ./node_modules/.bin/codecov

157
AUTHORS
View File

@ -1,41 +1,176 @@
TJ Holowaychuk <tj@vision-media.ca>
Jonathan Ong <jonathanrichardong@gmail.com>
dead_horse <dead_horse@qq.com>
Julian Gruber <julian@juliangruber.com>
pana <pana.wang@outlook.com>
小菜 <xtx1130@gmail.com>
Aaron Heckmann <aaron.heckmann+github@gmail.com>
Adam L <skyros@gmail.com>
Adam Lau <skyros@gmail.com>
Aesop Wolf <aesopwolf@users.noreply.github.com>
AlexeyKhristov <AlexeyKhristov@users.noreply.github.com>
Alexsey <agat00@gmail.com>
Amit Portnoy <amit.portnoy@gmail.com>
Anton Harniakou <anton.harniakou@gmail.com>
Arjun <arjun453@gmail.com>
Asiel Leal <lealceldeiro@gmail.com>
Avindra Goolcharan <aavindraa@gmail.com>
Bartol Karuza <bartol.k@gmail.com>
Ben Reinhart <breinhart@groupon.com>
Bernie Stern <bernzs@gmail.com>
Bryan Bess <squarejaw@bsbess.com>
C.T. Lin <chentsulin@gmail.com>
Chiahao Lin <purepennons@users.noreply.github.com>
Chris Tarquini <chris@ilsken.com>
Christoffer Hallas <hallas@users.noreply.github.com>
Clark Du <clark.duxin@gmail.com>
Darren Cauthon <darren@cauthon.com>
Debjeet Biswas <debjeet@vxtindia.com>
Dmitry Mazuro <dmitry.mazuro@icloud.com>
Douglas Christopher Wilson <doug@somethingdoug.com>
Eivind Fjeldstad <eivind.fjeldstad@gmail.com>
Equim <sayaka@ekyu.moe>
Fangdun Cai <fundon@users.noreply.github.com>
Felix Becker <felix.b@outlook.com>
Filip Skokan <panva.ip@gmail.com>
Francisco Presencia <franciscop@users.noreply.github.com>
George Chung <Gerhut@GMail.com>
Gilles De Mey <gilles.de.mey@gmail.com>
Grand <sungg12138@163.com>
Guilherme Pacheco <guilherme.f.pacheco@hotmail.com>
HanHor Wu <hanhor.wu@gmail.com>
Hartley Melamed <hartley@melamed.biz>
Hrvoje Šimić <hrvoje@twobucks.co>
Hugh Kennedy <hughskennedy@gmail.com>
Ian Storm Taylor <ian@ianstormtaylor.com>
Yiyu He <dead_horse@qq.com>
Sonny Piers <sonny@fastmail.net>
yoshuawuyts <i@yoshuawuyts.com>
fengmk2 <fengmk2@gmail.com>
PatrickJS <github@gdi2290.com>
Ilkka Oksanen <iao@iki.fi>
Ivan Kleshnin <ivan@paqmind.com>
Ivan Lyons <iliyang.cn@gmail.com>
Jacob Bass <jacob@jacobbass.net>
JamesWang <likegun94@gmail.com>
Jan Buschtöns <buschtoens@gmail.com>
Jan Carlo Viray <virayjancarlo@yahoo.com>
Jason Macgowan <jason.macgowan@icloud.com>
Jed Schmidt <where@jed.is>
Jeff Moore <jeff@procata.com>
Jesus Rodriguez <foxandxss@gmail.com>
Jesús Rodríguez Rodríguez <Foxandxss@gmail.com>
Jingwei "John" Liu <liujingwei@gmail.com>
Johan Bergström <bugs@bergstroem.nu>
Jonas Zhang <106856363@qq.com>
Jonathan Ong <jonathanrichardong@gmail.com>
Jonathan Ong <me@jongleberry.com>
Joseph Lin <josephlin55555@gmail.com>
Julian Gruber <julian@juliangruber.com>
Kareem Kwong <kareem.kwong@gmail.com>
Karl Böhlmark <karl.bohlmark@gmail.com>
Kenneth Ormandy <kenneth@chloi.io>
Kim Joar Bekkelund <kjbekkelund@gmail.com>
Kwyn Alice Meagher <kwyn.meagher@gmail.com>
Kyle Suss <susskyle@gmail.com>
Lee Bousfield <ljbousfield@gmail.com>
Louis DeScioli <louis.descioli@gmail.com>
Luke Bousfield <math.master.champion@gmail.com>
Malcolm <noinkling@users.noreply.github.com>
Marceli.no <me@marceli.no>
Mars Wong <marswong618@gmail.com>
Martin Iwanowski <martin@iwanowski.se>
Martin Iwanowski <me@fl0w.io>
Martin fl0w Iwanowski <martin@iwanowski.se>
Matheus Azzi <matheuslazzi@gmail.com>
Mathieu Gallé-Tessonneau <mathieu.galletessonneau@gmail.com>
Matthew Chase Whittemore <matthew@socialtables.com>
Matthew King <mking@users.noreply.github.com>
Matthew Mueller <mattmuelle@gmail.com>
Mengdi Gao <gaomdev@gmail.com>
Michaël Zasso <mic.besace@gmail.com>
Michał Gołębiowski-Owczarek <m.goleb@gmail.com>
Nathan Rajlich <nathan@tootallnate.net>
New Now Nohow <empty@cqdr.es>
Nick McCurdy <nick@nickmccurdy.com>
Nicolae Vartolomei <nvartolomei@gmail.com>
PatrickJS <github@gdi2290.com>
Paul Anderson <thesamuraipanda@gmail.com>
Pedro Pablo Aste Kompen <wachunei@gmail.com>
Peeyush Kushwaha <peeyush.p97@gmail.com>
Phillip Alexander <git@phillipalexander.io>
PlasmaPower <ljbousfield@gmail.com>
Prayag Verma <prayag.verma@gmail.com>
Qiming zhao <chemzqm@gmail.com>
Remek Ambroziak <remek.ambroziak@gmail.com>
Riceball LEE <snowyu.lee@gmail.com>
Richard Marmorstein <twitchard@users.noreply.github.com>
Rico Sta. Cruz <rstacruz@users.noreply.github.com>
Robert Sköld <robert@publicclass.se>
Robin Pokorný <me@robinpokorny.com>
Ruben Bridgewater <ruben@bridgewater.de>
Rui Marinho <rpm@seegno.com>
Rui Marinho <ruipmarinho@gmail.com>
Ryunosuke SATO <tricknotes.rs@gmail.com>
Saad Quadri <saad@saadq.com>
Santiago Sotomayor <sansoto2003@yahoo.com.ar>
Sergei Osipov <hcz@users.noreply.github.com>
Shaun Warman <shaunwarman1@gmail.com>
Shawn Cheung <958033967@qq.com>
Shawn Sit <xueqingxiao@gmail.com>
Slobodan Stojanovic <slobodan@cloudhorizon.com>
Sonny Piers <sonny@fastmail.net>
Sterling Williams <sterlingw@qualtrics.com>
Stéphane Bisinger <stephane.bisinger@protonmail.com>
TJ Holowaychuk <tj@apex.sh>
TJ Holowaychuk <tj@vision-media.ca>
Taehwan, No <taehwanno.dev@gmail.com>
Tejas Manohar <me@tejas.io>
Teoman Soygul <teo@soygul.com>
Thiago Lagden <lagden@gmail.com>
Tiago Ribeiro <tlr@seegno.com>
Tim Schaub <tim.schaub@gmail.com>
Todor Stoychev <pretodor@gmail.com>
Tomas Ruud <tomasruud@users.noreply.github.com>
Travis Jeffery <tj@travisjeffery.com>
Usman Hussain <usmandap@gmail.com>
Veselin Todorov <veselin@veselin.bg>
Wang Dàpéng <wonderfuly@gmail.com>
Xavier Damman <xdamman@gmail.com>
Xiang Gao <geekplux@qq.com>
Yanick Rochon <yanick.rochon@gmail.com>
Yazhong Liu <l900422@vip.qq.com>
Yazhong Liu <yorkiefixer@gmail.com>
Yiyu He <dead-horse@users.noreply.github.com>
Yiyu He <dead_horse@qq.com>
Yoshua Wuyts <yoshuawuyts@gmail.com>
Yu Qi <iyuq@outlook.com>
Yu Qi <njuyuqi@gmail.com>
Zack Tanner <zacktanner@gmail.com>
alsotang <alsotang@gmail.com>
bananaappletw <bananaappletw@gmail.com>
bhanuc <bhanuc@iitk.ac.in>
blaz <blaz@menems.net>
broucz <broucapierre@gmail.com>
d3v <cr1s@users.noreply.github.com>
dead-horse <dead_horse@qq.com>
dead_horse <dead_horse@qq.com>
designgrill <anshul@designgrill.com>
fengmk2 <fengmk2@gmail.com>
fengmk2 <m@fengmk2.com>
frank <frankxin93@hotmail.com>
fundon <cfddream@gmail.com>
gyson <eilian.yunsong@gmail.com>
haoxin <coderhaoxin@outlook.com>
haoxin <haoxins@icloud.com>
iamchenxin <iamchenxin@gmail.com>
initial-wu <initial-wu@outlook.com>
jeromew <jerome.wagner@m4x.org>
joehecn <leanbrown@live.cn>
jongleberry <jonathanong@users.noreply.github.com>
jongleberry <me@jongleberry.com>
llambda <xxgsoftware@gmail.com>
mako-taco <jake.y.scott@gmail.com>
mdemo <mds@xue.bi>
nicoder <nicolas.dermine@gmail.com>
superchink <superchink@gmail.com>
nswbmw <gxqzk@126.com>
pana <pana.wang@outlook.com>
qingming <358242939@qq.com>
song <xiongsongsong@outlook.com>
superchink <superchink@gmail.com>
tmilewski <tmilewski@gmail.com>
yoshuawuyts <i@yoshuawuyts.com>
yosssi <yoshida.keiji.84@gmail.com>
zensh <admin@zensh.com>
ziyunfei <446240525@qq.com>
石发磊 <sshsfl@yeah.net>

74
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,74 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at tj@tjholowaychuk.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View File

@ -1,262 +1,41 @@
0.21.0 / 2015-05-23
2.8.7 / 2019-10-11
==================
* empty `request.query` objects are now always the same instance
* bump `fresh@0.3.0`
Re-implemented accepts, acceptsEncoding, acceptsLanguage and etc.
0.20.0 / 2015-04-30
2.8.6 / 2019-10-11
==================
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
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)

View File

@ -1,6 +1,6 @@
(The MIT License)
Copyright (c) 2015 Koa contributors
Copyright (c) 2019 Koa contributors
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the

View File

@ -1,50 +0,0 @@
SRC = lib/*.js
include node_modules/make-lint/index.mk
BIN = iojs
ifeq ($(findstring io.js, $(shell which node)),)
BIN = node
endif
ifeq (node, $(BIN))
FLAGS = --harmony-generators
endif
TESTS = test/application \
test/context/* \
test/request/* \
test/response/* \
test/experimental/index.js
test:
@NODE_ENV=test $(BIN) $(FLAGS) \
./node_modules/.bin/_mocha \
--require should \
$(TESTS) \
--bail
test-cov:
@NODE_ENV=test $(BIN) $(FLAGS) \
./node_modules/.bin/istanbul cover \
./node_modules/.bin/_mocha \
-- -u exports \
--require should \
$(TESTS) \
--bail
test-travis:
@NODE_ENV=test $(BIN) $(FLAGS) \
./node_modules/.bin/istanbul cover \
./node_modules/.bin/_mocha \
--report lcovonly \
-- -u exports \
--require should \
$(TESTS) \
--bail
bench:
@$(MAKE) -C benchmarks
.PHONY: test bench

223
Readme.md
View File

@ -1,96 +1,195 @@
<img src="https://dl.dropboxusercontent.com/u/6396913/koa/logo.png" alt="koa middleware framework for nodejs" width="255px" />
## 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]
[![PR's Welcome][pr-welcoming-image]][pr-welcoming-url]
Expressive middleware for node.js using generators via [co](https://github.com/visionmedia/co)
to make web applications and APIs more enjoyable to write. Koa's middleware flow in a stack-like manner allowing you to perform actions downstream, then filter and manipulate the response upstream. Koa's use of generators also greatly increases the readability and robustness of your application.
## What is Koa
Only methods that are common to nearly all HTTP servers are integrated directly into Koa's small ~550 SLOC codebase. This
includes things like content-negotiation, normalization of node inconsistencies, redirection, and a few others.
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.
No middleware are bundled with koa.
Only methods that are common to nearly all HTTP servers are integrated directly into Koa's small ~570 SLOC codebase. This
includes things like content negotiation, normalization of node inconsistencies, redirection, and a few others.
Koa is not bundled with any middleware.
## Installation
Koa requires __node v7.6.0__ or higher for ES2015 and async function support.
```
$ npm install koa
$ npm install koa-lite
```
Koa is supported in all versions of [iojs](https://iojs.org) without any flags.
To use Koa with node, you must be running __node 0.11.16__ or higher for generator and promise support, and must run node(1)
with the `--harmony-generators` or `--harmony` flag.
## Community
- [API](docs/api/index.md) documentation
- [Badgeboard](https://koajs.github.io/badgeboard) and list of official modules
- [Examples](https://github.com/koajs/examples)
- [Middleware](https://github.com/koajs/koa/wiki) list
- [Wiki](https://github.com/koajs/koa/wiki)
- [G+ Community](https://plus.google.com/communities/101845768320796750641)
- [Reddit Community](http://reddit.com/r/koajs)
- [Mailing list](https://groups.google.com/forum/#!forum/koajs)
- [Guide](docs/guide.md)
- [FAQ](docs/faq.md)
- [中文文档](https://github.com/turingou/koa-guide)
- __#koajs__ on freenode
## Getting started
- [Kick-Off-Koa](https://github.com/koajs/kick-off-koa) - An intro to koa via a set of self-guided workshops.
- [Workshop](https://github.com/koajs/workshop) - A workshop to learn the basics of koa, Express' spiritual successor.
- [Introduction Screencast](http://knowthen.com/episode-3-koajs-quickstart-guide/) - An introduction to installing and getting started with Koa
## Example
## Hello Koa
```js
var koa = require('koa');
var app = koa();
// logger
app.use(function *(next){
var start = new Date;
yield next;
var ms = new Date - start;
console.log('%s %s - %s', this.method, this.url, ms);
});
const Koa = require('koa-lite');
const app = new Koa();
// response
app.use(function *(){
this.body = 'Hello World';
app.use(ctx => {
ctx.body = 'Hello Koa';
});
app.listen(3000);
```
## Getting started
- [Kick-Off-Koa](https://github.com/koajs/kick-off-koa) - An intro to Koa via a set of self-guided workshops.
- [Workshop](https://github.com/koajs/workshop) - A workshop to learn the basics of Koa, Express' spiritual successor.
- [Introduction Screencast](http://knowthen.com/episode-3-koajs-quickstart-guide/) - An introduction to installing and getting started with Koa
## Middleware
Koa is a middleware framework that can take two different kinds of functions as middleware:
* async function
* common function
Here is an example of logger middleware with each of the different functions:
### ___async___ functions (node v7.6+)
```js
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
```
### Common function
```js
// Middleware normally takes two parameters (ctx, next), ctx is the context for one request,
// next is a function that is invoked to execute the downstream middleware. It returns a Promise with a then function for running code after completion.
app.use((ctx, next) => {
const start = Date.now();
return next().then(() => {
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
});
```
## Context, Request and Response
Each middleware receives a Koa `Context` object that encapsulates an incoming
http message and the corresponding response to that message. `ctx` is often used
as the parameter name for the context object.
```js
app.use(async (ctx, next) => { await next(); });
```
Koa provides a `Request` object as the `request` property of the `Context`.
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.
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)
.
Koa's pattern of delegating to Node's request and response objects rather than extending them
provides a cleaner interface and reduces conflicts between different middleware and with Node
itself as well as providing better support for stream handling. The `IncomingMessage` can still be
directly accessed as the `req` property on the `Context` and `ServerResponse` can be directly
accessed as the `res` property on the `Context`.
Here is an example using Koa's `Response` object to stream a file as the response body.
```js
app.use(async (ctx, next) => {
await next();
ctx.response.type = 'xml';
ctx.response.body = fs.createReadStream('really_large.xml');
});
```
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`.
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).
## Koa Application
The object created when executing `new Koa()` is known as the Koa application object.
The application object is Koa's interface with node's http server and handles the registration
of middleware, dispatching to the middleware from http, default error handling, as well as
configuration of the context, request and response objects.
Learn more about the application object in the [Application API Reference](docs/api/index.md).
## Documentation
- [Usage Guide](docs/guide.md)
- [Error Handling](docs/error-handling.md)
- [Koa for Express Users](docs/koa-vs-express.md)
- [FAQ](docs/faq.md)
- [API documentation](docs/api/index.md)
## Troubleshooting
Check the [Troubleshooting Guide](docs/troubleshooting.md) or [Debugging Koa](docs/guide.md#debugging-koa) in
the general Koa guide.
## Running tests
```
$ make test
$ npm test
```
## Reporting vulnerabilities
To report a security vulnerability, please do not open an issue, as this notifies attackers
of the vulnerability. Instead, please email [dead_horse](mailto:heyiyu.deadhorse@gmail.com) and [jonathanong](mailto:me@jongleberry.com) to
disclose.
## Authors
- [TJ Holowaychuk](https://github.com/tj)
- [Jonathan Ong](https://github.com/jonathanong)
- [Julian Gruber](https://github.com/juliangruber)
- [Yiyu He](https://github.com/dead-horse)
See [AUTHORS](AUTHORS).
## Community
- [Badgeboard](https://koajs.github.io/badgeboard) and list of official modules
- [Examples](https://github.com/koajs/examples)
- [Middleware](https://github.com/koajs/koa/wiki) list
- [Wiki](https://github.com/koajs/koa/wiki)
- [G+ Community](https://plus.google.com/communities/101845768320796750641)
- [Reddit Community](https://www.reddit.com/r/koajs)
- [Mailing list](https://groups.google.com/forum/#!forum/koajs)
- [中文文档 v1.x](https://github.com/guo-yu/koa-guide)
- [中文文档 v2.x](https://github.com/demopark/koa-docs-Zh-CN)
- __[#koajs]__ on freenode
# License
MIT
[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://npmjs.org/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/coveralls/koajs/koa/master.svg?style=flat-square
[coveralls-url]: https://coveralls.io/r/koajs/koa?branch=master
[gitter-image]: https://badges.gitter.im/Join%20Chat.svg
[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
[pr-welcoming-image]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square
[pr-welcoming-url]: https://github.com/koajs/koa/pull/new

View File

@ -1,26 +1,23 @@
all: middleware experimental
all: middleware
middleware:
@./run 1 $@
@./run 5 $@
@./run 10 $@
@./run 15 $@
@./run 20 $@
@./run 30 $@
@./run 50 $@
@./run 100 $@
@./run 1 false $@
@./run 5 false $@
@./run 10 false $@
@./run 15 false $@
@./run 20 false $@
@./run 30 false $@
@./run 50 false $@
@./run 100 false $@
@./run 1 true $@
@./run 5 true $@
@./run 10 true $@
@./run 15 true $@
@./run 20 true $@
@./run 30 true $@
@./run 50 true $@
@./run 100 true $@
@echo
experimental:
@./run 1 $@
@./run 5 $@
@./run 10 $@
@./run 15 $@
@./run 20 $@
@./run 30 $@
@./run 50 $@
@./run 100 $@
@echo
.PHONY: all middleware experimental
.PHONY: all middleware

View File

@ -1,26 +0,0 @@
var http = require('http');
var koa = require('../..');
var app = koa();
app.experimental = true;
// number of middleware
var n = parseInt(process.env.MW || '1', 10);
console.log(' %s async middleware', n);
while (n--) {
app.use(async function (next){
await next;
});
}
var body = new Buffer('Hello World');
app.use(async function (next){
await next;
this.body = body;
});
app.listen(3333);

View File

@ -1,6 +0,0 @@
// support async await by babel
require('babel/register')({
optional: ['asyncToGenerator']
});
require('./async');

View File

@ -1,24 +1,30 @@
var http = require('http');
var koa = require('..');
var app = koa();
'use strict';
const Koa = require('..');
const app = new Koa();
// number of middleware
var n = parseInt(process.env.MW || '1', 10);
console.log(' %s middleware', n);
let n = parseInt(process.env.MW || '1', 10);
let useAsync = process.env.USE_ASYNC === 'true';
console.log(` ${n}${useAsync ? ' async' : ''} middleware`);
while (n--) {
app.use(function *(next){
yield *next;
});
if (useAsync) {
app.use(async(ctx, next) => await next());
} else {
app.use((ctx, next) => next());
}
}
var body = new Buffer('Hello World');
const body = Buffer.from('Hello World');
app.use(function *(next){
yield *next;
this.body = body;
});
if (useAsync) {
app.use(async(ctx, next) => { await next(); ctx.body = body; });
} else {
app.use((ctx, next) => next().then(() => ctx.body = body));
}
app.listen(3333);

View File

@ -1,7 +1,7 @@
#!/usr/bin/env bash
echo
MW=$1 node --harmony $2 &
MW=$1 USE_ASYNC=$2 node $3 &
pid=$!
sleep 2

View File

@ -8,14 +8,14 @@
which would force middleware to re-implement this common functionality.
A `Context` is created _per_ request, and is referenced in middleware
as the receiver, or the `this` identifier, as shown in the following
as the receiver, or the `ctx` identifier, as shown in the following
snippet:
```js
app.use(function *(){
this; // is the Context
this.request; // is a koa Request
this.response; // is a koa Response
app.use(async ctx => {
ctx; // is the Context
ctx.request; // is a Koa Request
ctx.response; // is a Koa Response
});
```
@ -44,63 +44,46 @@ app.use(function *(){
### ctx.request
A koa `Request` object.
A Koa `Request` object.
### ctx.response
A koa `Response` object.
A Koa `Response` object.
### ctx.state
The recommended namespace for passing information through middleware and to your frontend views.
```js
this.state.user = yield User.find(id);
ctx.state.user = await User.find(id);
```
### ctx.app
Application instance reference.
### ctx.cookies.get(name, [options])
### ctx.app.emit
Get cookie `name` with `options`:
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.
- `signed` the cookie requested should be signed
koa uses the [cookies](https://github.com/jed/cookies) module where options are simply passed.
### ctx.cookies.set(name, value, [options])
Set cookie `name` to `value` with `options`:
- `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
koa uses the [cookies](https://github.com/jed/cookies) module where options are simply passed.
### ctx.throw([msg], [status], [properties])
### ctx.throw([status], [msg], [properties])
Helper method to throw an error with a `.status` property
defaulting to `500` that will allow Koa to respond appropriately.
The following combinations are allowed:
```js
this.throw(403);
this.throw('name required', 400);
this.throw(400, 'name required');
this.throw('something exploded');
ctx.throw(400);
ctx.throw(400, 'name required');
ctx.throw(400, 'name required', { user: user });
```
For example `this.throw('name required', 400)` is equivalent to:
For example `ctx.throw(400, 'name required')` is equivalent to:
```js
var err = new Error('name required');
const err = new Error('name required');
err.status = 400;
err.expose = true;
throw err;
```
@ -113,27 +96,26 @@ throw err;
You may optionally pass a `properties` object which is merged into the error as-is, useful for decorating machine-friendly errors which are reported to the requester upstream.
```js
this.throw(401, 'access_denied', { user: user });
this.throw('access_denied', { user: user });
ctx.throw(401, 'access_denied', { user: user });
```
koa uses [http-errors](https://github.com/jshttp/http-errors) to create errors.
Koa uses [http-errors](https://github.com/jshttp/http-errors) to create errors. `status` should only be passed as the first parameter.
### ctx.assert(value, [msg], [status], [properties])
### ctx.assert(value, [status], [msg], [properties])
Helper method to throw an error similar to `.throw()`
when `!value`. Similar to node's [assert()](http://nodejs.org/api/assert.html)
method.
```js
this.assert(this.state.user, 401, 'User not found. Please login!');
ctx.assert(ctx.state.user, 401, 'User not found. Please login!');
```
koa uses [http-assert](https://github.com/jshttp/http-assert) for assertions.
Koa uses [http-assert](https://github.com/jshttp/http-assert) for assertions.
### ctx.respond
To bypass Koa's built-in response handling, you may explicitly set `this.respond = false;`. Use this if you want to write to the raw `res` object instead of letting Koa handle the response for you.
To bypass Koa's built-in response handling, you may explicitly set `ctx.respond = false;`. Use this if you want to write to the raw `res` object instead of letting Koa handle the response for you.
Note that using this is __not__ supported by Koa. This may break intended functionality of Koa middleware and Koa itself. Using this property is considered a hack and is only a convenience to those wishing to use traditional `fn(req, res)` functions and middleware within Koa.
@ -148,6 +130,7 @@ koa uses [http-assert](https://github.com/jshttp/http-assert) for assertions.
- `ctx.url`
- `ctx.url=`
- `ctx.originalUrl`
- `ctx.origin`
- `ctx.href`
- `ctx.path`
- `ctx.path=`
@ -166,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

@ -1,21 +1,41 @@
# Installation
Koa is supported in all versions of [iojs](https://iojs.org) without any flags.
Koa requires __node v7.6.0__ or higher for ES2015 and async function support.
To use Koa with node, you must be running __node 0.11.16__ or higher for generator and promise support, and must run node(1)
with the `--harmony-generators` or `--harmony` flag.
You can quickly install a supposed version of node/iojs with your favorite version manager:
You can quickly install a supported version of node with your favorite version manager:
```bash
$ nvm install iojs
$ nvm install 7
$ npm i koa
$ node my-koa-app.js
```
## Async Functions with Babel
To use `async` functions in Koa in versions of node < 7.6, we recommend using [babel's require hook](http://babeljs.io/docs/usage/babel-register/).
```js
require('babel-register');
// require the rest of the app that needs to be transpiled after the hook
const app = require('./app');
```
To parse and transpile async functions,
you should at a minimum have the [transform-async-to-generator](http://babeljs.io/docs/plugins/transform-async-to-generator/)
or [transform-async-to-module-method](http://babeljs.io/docs/plugins/transform-async-to-module-method/) plugins.
For example, in your `.babelrc` file, you should have:
```json
{
"plugins": ["transform-async-to-generator"]
}
```
You can also use the [env preset](http://babeljs.io/docs/plugins/preset-env/) with a target option `"node": "current"` instead.
# Application
A Koa application is an object containing an array of middleware generator functions
A Koa application is an object containing an array of middleware functions
which are composed and executed in a stack-like manner upon request. Koa is similar to many
other middleware systems that you may have encountered such as Ruby's Rack, Connect, and so on -
however a key design decision was made to provide high level "sugar" at the otherwise low-level
@ -28,12 +48,13 @@ $ node my-koa-app.js
The obligatory hello world application:
<!-- runkit:endpoint -->
```js
var koa = require('koa');
var app = koa();
const Koa = require('koa');
const app = new Koa();
app.use(function *(){
this.body = 'Hello World';
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
@ -43,43 +64,43 @@ app.listen(3000);
Koa middleware cascade in a more traditional way as you may be used to with similar tools -
this was previously difficult to make user friendly with node's use of callbacks.
However with generators we can achieve "true" middleware. Contrasting Connect's implementation which
simply passes control through series of functions until one returns, Koa yields "downstream", then
However with async functions we can achieve "true" middleware. Contrasting Connect's implementation which
simply passes control through series of functions until one returns, Koa invoke "downstream", then
control flows back "upstream".
The following example responds with "Hello World", however first the request flows through
the `x-response-time` and `logging` middleware to mark when the request started, then continue
to yield control through the response middleware. When a middleware invokes `yield next`
to yield control through the response middleware. When a middleware invokes `next()`
the function suspends and passes control to the next middleware defined. After there are no more
middleware to execute downstream, the stack will unwind and each middleware is resumed to perform
its upstream behaviour.
<!-- runkit:endpoint -->
```js
var koa = require('koa');
var app = koa();
// x-response-time
app.use(function *(next){
var start = new Date;
yield next;
var ms = new Date - start;
this.set('X-Response-Time', ms + 'ms');
});
const Koa = require('koa');
const app = new Koa();
// logger
app.use(function *(next){
var start = new Date;
yield next;
var ms = new Date - start;
console.log('%s %s - %s', this.method, this.url, ms);
app.use(async (ctx, next) => {
await next();
const rt = ctx.response.get('X-Response-Time');
console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});
// x-response-time
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});
// response
app.use(function *(){
this.body = 'Hello World';
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
@ -90,14 +111,25 @@ app.listen(3000);
Application settings are properties on the `app` instance, currently
the following are supported:
- `app.name` optionally give your application a name
- `app.env` defaulting to the __NODE_ENV__ or "development"
- `app.proxy` when true proxy header fields will be trusted
- `app.subdomainOffset` offset of `.subdomains` to ignore [2]
You can pass the settings to the constructor:
```js
const Koa = require('koa');
const app = new Koa({ proxy: true });
```
or dynamically:
```js
const Koa = require('koa');
const app = new Koa();
app.proxy = true;
```
## app.listen(...)
A Koa application is not a 1-to-1 representation of a HTTP server.
A Koa application is not a 1-to-1 representation of an HTTP server.
One or more Koa applications may be mounted together to form larger
applications with a single HTTP server.
@ -105,17 +137,17 @@ app.listen(3000);
`Server#listen()`. These arguments are documented on [nodejs.org](http://nodejs.org/api/http.html#http_server_listen_port_hostname_backlog_callback). The following is a useless Koa application bound to port `3000`:
```js
var koa = require('koa');
var app = koa();
const Koa = require('koa');
const app = new Koa();
app.listen(3000);
```
The `app.listen(...)` method is simply sugar for the following:
```js
var http = require('http');
var koa = require('koa');
var app = koa();
const http = require('http');
const Koa = require('koa');
const app = new Koa();
http.createServer(app.callback()).listen(3000);
```
@ -123,18 +155,19 @@ http.createServer(app.callback()).listen(3000);
or on multiple addresses:
```js
var http = require('http');
var koa = require('koa');
var app = koa();
const http = require('http');
const https = require('https');
const Koa = require('koa');
const app = new Koa();
http.createServer(app.callback()).listen(3000);
http.createServer(app.callback()).listen(3001);
https.createServer(app.callback()).listen(3001);
```
## app.callback()
Return a callback function suitable for the `http.createServer()`
method to handle a request.
You may also use this callback function to mount your koa app in a
You may also use this callback function to mount your Koa app in a
Connect/Express app.
## app.use(function)
@ -142,42 +175,46 @@ http.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=
## app.context
Set signed cookie keys.
`app.context` is the prototype from which `ctx` is created.
You may add additional properties to `ctx` by editing `app.context`.
This is useful for adding properties or methods to `ctx` to be used across your entire app,
which may be more performant (no middleware) and/or easier (fewer `require()`s)
at the expense of relying more on `ctx`, which could be considered an anti-pattern.
These are passed to [KeyGrip](https://github.com/jed/keygrip),
however you may also pass your own `KeyGrip` instance. For
example the following are acceptable:
For example, to add a reference to your database from `ctx`:
```js
app.keys = ['im a newer secret', 'i like turtle'];
app.keys = new KeyGrip(['im a newer secret', 'i like turtle'], 'sha256');
```
app.context.db = db();
These keys may be rotated and are used when signing cookies
with the `{ signed: true }` option:
```js
this.cookies.set('name', 'tobi', { signed: true });
```
## Error Handling
By default outputs all errors to stderr unless __NODE_ENV__ is "test". To perform custom error-handling logic such as centralized logging you
can add an "error" event listener:
```js
app.on('error', function(err){
log.error('server error', err);
app.use(async ctx => {
console.log(ctx.db);
});
```
If an error in the req/res cycle and it is _not_ possible to respond to the client, the `Context` instance is also passed:
Note:
- Many properties on `ctx` are defined using getters, setters, and `Object.defineProperty()`. You can only edit these properties (not recommended) by using `Object.defineProperty()` on `app.context`. See https://github.com/koajs/koa/issues/652.
- Mounted apps currently use their parent's `ctx` and settings. Thus, mounted apps are really just groups of middleware.
## Error Handling
By default outputs all errors to stderr unless `app.silent` is `true`.
The default error handler also won't output errors when `err.status` is `404` or `err.expose` is `true`.
To perform custom error-handling logic such as centralized logging you can add an "error" event listener:
```js
app.on('error', function(err, ctx){
log.error('server error', err, ctx);
app.on('error', err => {
log.error('server error', err)
});
```
If an error is in the req/res cycle and it is _not_ possible to respond to the client, the `Context` instance is also passed:
```js
app.on('error', (err, ctx) => {
log.error('server error', err, ctx)
});
```

View File

@ -8,12 +8,21 @@
### request.header
Request header object.
Request header object. This is the same as the [`headers`](https://nodejs.org/api/http.html#http_message_headers) field
on node's [`http.IncomingMessage`](https://nodejs.org/api/http.html#http_class_http_incomingmessage).
### request.header=
Set request header object.
### request.headers
Request header object. Alias as `request.header`.
### request.headers=
Set request header object. Alias as `request.header=`.
### request.method
Request method.
@ -39,12 +48,21 @@
Get request original URL.
### request.origin
Get origin of URL, include `protocol` and `host`.
```js
ctx.request.origin
// => http://example.com
```
### request.href
Get full request URL, include `protocol`, `host` and `url`.
```js
this.request.href
ctx.request.href;
// => http://example.com/foo/bar?q=1
```
@ -81,13 +99,21 @@ this.request.href
Get hostname when present. Supports `X-Forwarded-Host`
when `app.proxy` is __true__, otherwise `Host` is used.
If host is IPv6, Koa delegates parsing to
[WHATWG URL API](https://nodejs.org/dist/latest-v8.x/docs/api/url.html#url_the_whatwg_url_api),
*Note* This may impact performance.
### request.URL
Get WHATWG parsed URL object.
### request.type
Get request `Content-Type` void of parameters such as "charset".
```js
var ct = this.request.type;
const ct = ctx.request.type;
// => "image/png"
```
@ -96,7 +122,7 @@ var ct = this.request.type;
Get request charset when present, or `undefined`:
```js
this.request.charset
ctx.request.charset;
// => "utf-8"
```
@ -121,7 +147,7 @@ this.request.charset
setter does _not_ support nested objects.
```js
this.query = { next: '/login' };
ctx.query = { next: '/login' };
```
### request.fresh
@ -130,17 +156,19 @@ this.query = { next: '/login' };
method is for cache negotiation between `If-None-Match` / `ETag`, and `If-Modified-Since` and `Last-Modified`. It should be referenced after setting one or more of these response headers.
```js
this.set('ETag', '123');
// freshness check requires status 20x or 304
ctx.status = 200;
ctx.set('ETag', '123');
// cache is ok
if (this.fresh) {
this.status = 304;
if (ctx.fresh) {
ctx.status = 304;
return;
}
// cache is stale
// fetch new data
this.body = yield db.find('something');
ctx.body = await db.find('something');
```
### request.stale
@ -154,7 +182,7 @@ this.body = yield db.find('something');
### request.secure
Shorthand for `this.protocol == "https"` to check if a request was
Shorthand for `ctx.protocol == "https"` to check if a request was
issued via TLS.
### request.ip
@ -177,177 +205,42 @@ this.body = yield db.find('something');
parts of the host. This can be changed by setting `app.subdomainOffset`.
For example, if the domain is "tobi.ferrets.example.com":
If `app.subdomainOffset` is not set, this.subdomains is `["ferrets", "tobi"]`.
If `app.subdomainOffset` is 3, this.subdomains is `["tobi"]`.
If `app.subdomainOffset` is not set, `ctx.subdomains` is `["ferrets", "tobi"]`.
If `app.subdomainOffset` is 3, `ctx.subdomains` is `["tobi"]`.
### request.is(types...)
Check if the incoming request contains the "Content-Type"
header field, and it contains any of the give mime `type`s.
If there is no request body, `undefined` is returned.
If there is no request body, `null` is returned.
If there is no content type, or the match fails `false` is returned.
Otherwise, it returns the matching content-type.
```js
// With Content-Type: text/html; charset=utf-8
this.is('html'); // => 'html'
this.is('text/html'); // => 'text/html'
this.is('text/*', 'text/html'); // => 'text/html'
ctx.is('html'); // => 'html'
ctx.is('text/html'); // => 'text/html'
ctx.is('text/*', 'text/html'); // => 'text/html'
// When Content-Type is application/json
this.is('json', 'urlencoded'); // => 'json'
this.is('application/json'); // => 'application/json'
this.is('html', 'application/*'); // => 'application/json'
ctx.is('json', 'urlencoded'); // => 'json'
ctx.is('application/json'); // => 'application/json'
ctx.is('html', 'application/*'); // => 'application/json'
this.is('html'); // => false
ctx.is('html'); // => false
```
For example if you want to ensure that
only images are sent to a given route:
```js
if (this.is('image/*')) {
if (ctx.is('image/*')) {
// process
} else {
this.throw(415, 'images only!');
ctx.throw(415, 'images only!');
}
```
### 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
this.accepts('html');
// => "html"
// Accept: text/*, application/json
this.accepts('html');
// => "html"
this.accepts('text/html');
// => "text/html"
this.accepts('json', 'text');
// => "json"
this.accepts('application/json');
// => "application/json"
// Accept: text/*, application/json
this.accepts('image/png');
this.accepts('png');
// => false
// Accept: text/*;q=.5, application/json
this.accepts(['html', 'json']);
this.accepts('html', 'json');
// => "json"
// No Accept header
this.accepts('html', 'json');
// => "html"
this.accepts('json', 'html');
// => "json"
```
You may call `this.accepts()` as many times as you like,
or use a switch:
```js
switch (this.accepts('json', 'html', 'text')) {
case 'json': break;
case 'html': break;
case 'text': break;
default: this.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
this.acceptsEncodings('gzip', 'deflate', 'identity');
// => "gzip"
this.acceptsEncodings(['gzip', 'deflate', 'identity']);
// => "gzip"
```
When no arguments are given all accepted encodings
are returned as an array:
```js
// Accept-Encoding: gzip, deflate
this.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
this.acceptsCharsets('utf-8', 'utf-7');
// => "utf-8"
this.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
this.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
this.acceptsLanguages('es', 'en');
// => "es"
this.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
this.acceptsLanguages();
// => ["es", "pt", "en"]
```
### request.idempotent
Check if the request is idempotent.
@ -358,4 +251,4 @@ this.acceptsLanguages();
### request.get(field)
Return request header.
Return request header with case-insensitive `field`.

View File

@ -17,11 +17,11 @@
### response.socket
Request socket.
Response socket. Points to net.Socket instance as `request.socket`.
### response.status
Get response status. By default, `response.status` is not set unlike node's `res.statusCode` which defaults to `200`.
Get response status. By default, `response.status` is set to `404` unlike node's `res.statusCode` which defaults to `200`.
### response.status=
@ -38,13 +38,16 @@
- 205 "reset content"
- 206 "partial content"
- 207 "multi-status"
- 208 "already reported"
- 226 "im used"
- 300 "multiple choices"
- 301 "moved permanently"
- 302 "moved temporarily"
- 302 "found"
- 303 "see other"
- 304 "not modified"
- 305 "use proxy"
- 307 "temporary redirect"
- 308 "permanent redirect"
- 400 "bad request"
- 401 "unauthorized"
- 402 "payment required"
@ -53,21 +56,20 @@
- 405 "method not allowed"
- 406 "not acceptable"
- 407 "proxy authentication required"
- 408 "request time-out"
- 408 "request timeout"
- 409 "conflict"
- 410 "gone"
- 411 "length required"
- 412 "precondition failed"
- 413 "request entity too large"
- 414 "request-uri too large"
- 413 "payload too large"
- 414 "uri too long"
- 415 "unsupported media type"
- 416 "requested range not satisfiable"
- 416 "range not satisfiable"
- 417 "expectation failed"
- 418 "i'm a teapot"
- 418 "I'm a teapot"
- 422 "unprocessable entity"
- 423 "locked"
- 424 "failed dependency"
- 425 "unordered collection"
- 426 "upgrade required"
- 428 "precondition required"
- 429 "too many requests"
@ -76,11 +78,11 @@
- 501 "not implemented"
- 502 "bad gateway"
- 503 "service unavailable"
- 504 "gateway time-out"
- 504 "gateway timeout"
- 505 "http version not supported"
- 506 "variant also negotiates"
- 507 "insufficient storage"
- 509 "bandwidth limit exceeded"
- 508 "loop detected"
- 510 "not extended"
- 511 "network authentication required"
@ -104,7 +106,7 @@ so you can make a correction.
### response.length
Return response Content-Length as a number when present, or deduce
from `this.body` when possible, or `undefined`.
from `ctx.body` when possible, or `undefined`.
### response.body
@ -117,11 +119,21 @@ so you can make a correction.
- `string` written
- `Buffer` written
- `Stream` piped
- `Object` json-stringified
- `Object` || `Array` json-stringified
- `null` no content response
If `response.status` has not been set, Koa will automatically set the status to `200` or `204`.
Koa doesn't guard against everything that could be put as a response body -- a function doesn't serialise meaningfully, returning a boolean may make sense based on your application, and while an error works, it may not work as intended as some properties of an error are not enumerable. We recommend adding middleware in your app that asserts body types per app. A sample middleware might be:
```js
app.use(async (ctx, next) => {
await next()
ctx.assert.equal('object', typeof ctx, 500, 'some dev did something wrong')
})
```
#### String
The Content-Type is defaulted to text/html or text/plain, both with
@ -136,16 +148,33 @@ If `response.status` has not been set, Koa will automatically set the status to
The Content-Type is defaulted to application/octet-stream.
Whenever a stream is set as the response body, `.onerror` is automatically added as a listener to the `error` event to catch any errors.
In addition, whenever the request is closed (even prematurely), the stream is destroyed.
If you do not want these two features, do not set the stream as the body directly.
For example, you may not want this when setting the body as an HTTP stream in a proxy as it would destroy the underlying connection.
See: https://github.com/koajs/koa/pull/612 for more information.
Here's an example of stream error handling without automatically destroying the stream:
```js
const PassThrough = require('stream').PassThrough;
app.use(async ctx => {
ctx.body = someHTTPStream.on('error', ctx.onerror).pipe(PassThrough());
});
```
#### Object
The Content-Type is defaulted to application/json.
The Content-Type is defaulted to application/json. This includes plain objects `{ foo: 'bar' }` and arrays `['foo', 'bar']`.
### response.get(field)
Get a response header field value with case-insensitive `field`.
```js
var etag = this.get('ETag');
const etag = ctx.response.get('ETag');
```
### response.set(field, value)
@ -153,14 +182,14 @@ var etag = this.get('ETag');
Set response header `field` to `value`:
```js
this.set('Cache-Control', 'no-cache');
ctx.set('Cache-Control', 'no-cache');
```
### response.append(field, value)
Append additional header `field` with value `val`.
```js
this.append('Link', '<http://127.0.0.1/>');
ctx.append('Link', '<http://127.0.0.1/>');
```
### response.set(fields)
@ -168,12 +197,14 @@ this.append('Link', '<http://127.0.0.1/>');
Set several response header `fields` with an object:
```js
this.set({
ctx.set({
'Etag': '1234',
'Last-Modified': date
});
```
This delegates to [setHeader](https://nodejs.org/dist/latest/docs/api/http.html#http_request_setheader_name_value) which sets or updates headers by specified keys and doesn't reset the entire header.
### response.remove(field)
Remove header `field`.
@ -183,7 +214,7 @@ this.set({
Get response `Content-Type` void of parameters such as "charset".
```js
var ct = this.type;
const ct = ctx.type;
// => "image/png"
```
@ -192,20 +223,19 @@ var ct = this.type;
Set response `Content-Type` via mime string or file extension.
```js
this.type = 'text/plain; charset=utf-8';
this.type = 'image/png';
this.type = '.png';
this.type = 'png';
ctx.type = 'text/plain; charset=utf-8';
ctx.type = 'image/png';
ctx.type = '.png';
ctx.type = 'png';
```
Note: when appropriate a `charset` is selected for you, for
example `response.type = 'html'` will default to "utf-8", however
when explicitly defined in full as `response.type = 'text/html'`
no charset is assigned.
example `response.type = 'html'` will default to "utf-8". If you need to overwrite `charset`,
use `ctx.set('Content-Type', 'text/html')` to set response header field to value directly.
### response.is(types...)
Very similar to `this.request.is()`.
Very similar to `ctx.request.is()`.
Check whether the response type is one of the supplied types.
This is particularly useful for creating middleware that
manipulate responses.
@ -214,18 +244,18 @@ this.type = 'png';
all HTML responses except for streams.
```js
var minify = require('html-minifier');
const minify = require('html-minifier');
app.use(function *minifyHTML(next){
yield next;
app.use(async (ctx, next) => {
await next();
if (!this.response.is('html')) return;
if (!ctx.response.is('html')) return;
var body = this.body;
let body = ctx.body;
if (!body || body.pipe) return;
if (Buffer.isBuffer(body)) body = body.toString();
this.body = minify(body);
ctx.body = minify(body);
});
```
@ -238,26 +268,26 @@ app.use(function *minifyHTML(next){
is not present `alt` or "/" is used.
```js
this.redirect('back');
this.redirect('back', '/index.html');
this.redirect('/login');
this.redirect('http://google.com');
ctx.redirect('back');
ctx.redirect('back', '/index.html');
ctx.redirect('/login');
ctx.redirect('http://google.com');
```
To alter the default status of `302`, simply assign the status
before or after this call. To alter the body, assign it after this call:
```js
this.status = 301;
this.redirect('/cart');
this.body = 'Redirecting to shopping cart';
ctx.status = 301;
ctx.redirect('/cart');
ctx.body = 'Redirecting to shopping cart';
```
### response.attachment([filename])
### response.attachment([filename], [options])
Set `Content-Disposition` to "attachment" to signal the client
to prompt for download. Optionally specify the `filename` of the
download.
download and some [options](https://github.com/jshttp/content-disposition#options).
### response.headerSent
@ -274,7 +304,7 @@ this.body = 'Redirecting to shopping cart';
You can either set it as a `Date` or date string.
```js
this.response.lastModified = new Date();
ctx.response.lastModified = new Date();
```
### response.etag=
@ -283,9 +313,13 @@ this.response.lastModified = new Date();
Note that there is no corresponding `response.etag` getter.
```js
this.response.etag = crypto.createHash('md5').update(this.body).digest('hex');
ctx.response.etag = crypto.createHash('md5').update(ctx.body).digest('hex');
```
### response.vary(field)
Vary on `field`.
### response.flushHeaders()
Flush any set headers, and begin the body.

61
docs/error-handling.md Normal file
View File

@ -0,0 +1,61 @@
# Error Handling
## Try-Catch
Using async functions means that you can try-catch `next`.
This example adds a `.status` to all errors:
```js
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
err.status = err.statusCode || err.status || 500;
throw err;
}
});
```
### Default Error Handler
The default error handler is essentially a try-catch at
the very beginning of the middleware chain. To use a
different error handler, simply put another try-catch at
the beginning of the middleware chain, and handle the error
there. However, the default error handler is good enough for
most use cases. It will use a status code of `err.status`,
or by default 500. If `err.expose` is true, then `err.message`
will be the reply. Otherwise, a message generated from the
error code will be used (e.g. for the code 500 the message
"Internal Server Error" will be used). All headers will be
cleared from the request, but any headers in `err.headers`
will then be set. You can use a try-catch, as specified
above, to add a header to this list.
Here is an example of creating your own error handler:
```js
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
// will only respond with JSON
ctx.status = err.statusCode || err.status || 500;
ctx.body = {
message: err.message
};
}
})
```
## The Error Event
Error event listeners can be specified with `app.on('error')`.
If no error listener is specified, a default error listener
is used. Error listener receive all errors that make their
way back through the middleware chain, if an error is caught
and not thrown again, it will not be passed to the error
listener. If no error event listener is specified, then
`app.onerror` will be used, which simply log the error unless
`error.expose`is true or `app.silent` is true or `error.status`
is 404.

View File

@ -16,16 +16,10 @@
## Does Koa replace Connect?
No, just a different take on similar functionality
now that generators allow us to write code with less
now that async functions allow us to write code with fewer
callbacks. Connect is equally capable, and some may still prefer it,
it's up to what you prefer.
## Do generators decrease performance?
Barely - check out the benchmarks in our readme, the numbers
are more than fine, and there's no substitute for proper
horizontal scaling.
## Does Koa include routing?
No - out of the box Koa has no form of routing, however
@ -41,15 +35,15 @@
## What custom properties do the Koa objects have?
Koa uses its own custom objects: `this`, `this.request`, and `this.response`.
Koa uses its own custom objects: `ctx`, `ctx.request`, and `ctx.response`.
These objects abstract node's `req` and `res` objects with convenience methods and getters/setters.
Generally, properties added to these objects must obey the following rules:
- They must be either very commonly used and/or must do something useful
- If a property exists as a setter, then it will also exist as a getter, but not vice versa
Many of `this.request` and `this.response`'s properties are delegated to `this`.
Many of `ctx.request` and `ctx.response`'s properties are delegated to `ctx`.
If it's a getter/setter, then both the getter and the setter will strictly
correspond to either `this.request` or `this.response`.
correspond to either `ctx.request` or `ctx.response`.
Please think about these rules before suggesting additional properties.

View File

@ -1,74 +1,54 @@
# Guide
This guide covers Koa topics are not directly API related, such as best practices for writing middleware,
application structure suggestions.
This guide covers Koa topics that are not directly API related, such as best practices for writing middleware and application structure suggestions. In these examples we use async functions as middleware - you can also use commonFunction or generatorFunction which will be a little different.
## Table of Contents
- [Writing Middleware](#writing-middleware)
- [Middleware Best Practices](#middleware-best-practices)
- [Middleware options](#middleware-options)
- [Named middleware](#named-middleware)
- [Combining multiple middleware with koa-compose](#combining-multiple-middleware-with-koa-compose)
- [Response Middleware](#response-middleware)
- [Async operations](#async-operations)
- [Debugging Koa](#debugging-koa)
## Writing Middleware
Koa middleware are simple functions which return a `GeneratorFunction`, and accept another. When
the middleware is run by an "upstream" middleware, it must manually `yield` to the "downstream" middleware.
Koa middleware are simple functions which return a `MiddlewareFunction` with signature (ctx, next). When
the middleware is run, it must manually invoke `next()` to run the "downstream" middleware.
For example if you wanted to track how long it takes for a request to propagate through Koa by adding an
`X-Response-Time` header field the middleware would look like the following:
```js
function *responseTime(next) {
var start = new Date;
yield next;
var ms = new Date - start;
this.set('X-Response-Time', ms + 'ms');
async function responseTime(ctx, next) {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
}
app.use(responseTime);
```
Here's another way to write the same thing, inline:
```js
app.use(function *(next){
var start = new Date;
yield next;
var ms = new Date - start;
this.set('X-Response-Time', ms + 'ms');
});
```
If you're a front-end developer you can think any code before `yield next;` as the "capture" phase,
while any code after is the "bubble" phase. This crude gif illustrates how ES6 generators allow us
If you're a front-end developer you can think any code before `next();` as the "capture" phase,
while any code after is the "bubble" phase. This crude gif illustrates how async function allow us
to properly utilize stack flow to implement request and response flows:
![koa middleware](/docs/middleware.gif)
![Koa middleware](/docs/middleware.gif)
1. Create a date to track duration
2. Yield control to the next middleware
3. Create another date to track response time
4. Yield control to the next middleware
5. Yield immediately since `contentLength` only works with responses
6. Yield upstream to Koa's noop middleware
7. Ignore setting the body unless the path is "/"
8. Set the response to "Hello World"
9. Ignore setting `Content-Length` when no body is present
10. Set the field
11. Output log line
12. Set `X-Response-Time` header field before response
13. Hand off to Koa to handle the response
Note that the final middleware (step __6__) yields to what looks to be nothing - it's actually
yielding to a no-op generator within Koa. This is so that every middleware can conform with the
same API, and may be placed before or after others. If you removed `yield next;` from the furthest
"downstream" middleware everything would function appropritaely, however it would no longer conform
to this behaviour.
For example this would be fine:
```js
app.use(function *response(){
if ('/' != this.url) return;
this.body = 'Hello World';
});
```
1. Create a date to track response time
2. Await control to the next middleware
3. Create another date to track duration
4. Await control to the next middleware
5. Set the response body to "Hello World"
6. Calculate duration time
7. Output log line
8. Calculate response time
9. Set `X-Response-Time` header field
10. Hand off to Koa to handle the response
Next we'll look at the best practices for creating Koa middleware.
@ -91,15 +71,15 @@ app.use(function *response(){
function logger(format) {
format = format || ':method ":url"';
return function *(next){
var str = format
.replace(':method', this.method)
.replace(':url', this.url);
return async function (ctx, next) {
const str = format
.replace(':method', ctx.method)
.replace(':url', ctx.url);
console.log(str);
yield next;
}
await next();
};
}
app.use(logger());
@ -112,132 +92,127 @@ app.use(logger(':method :url'));
```js
function logger(format) {
return function *logger(next){
return async function logger(ctx, next) {
}
};
}
```
### Combining multiple middleware
### Combining multiple middleware with koa-compose
Sometimes you want to "compose" multiple middleware into a single middleware for easy re-use or exporting. To do so, you may chain them together with `.call(this, next)`s, then return another function that yields the chain.
Sometimes you want to "compose" multiple middleware into a single middleware for easy re-use or exporting. You can use [koa-compose](https://github.com/koajs/compose)
```js
function *random(next) {
if ('/random' == this.path) {
this.body = Math.floor(Math.random()*10);
const compose = require('koa-compose');
async function random(ctx, next) {
if ('/random' == ctx.path) {
ctx.body = Math.floor(Math.random() * 10);
} else {
yield next;
await next();
}
};
function *backwards(next) {
if ('/backwards' == this.path) {
this.body = 'sdrawkcab';
async function backwards(ctx, next) {
if ('/backwards' == ctx.path) {
ctx.body = 'sdrawkcab';
} else {
yield next;
await next();
}
}
function *pi(next) {
if ('/pi' == this.path) {
this.body = String(Math.PI);
async function pi(ctx, next) {
if ('/pi' == ctx.path) {
ctx.body = String(Math.PI);
} else {
yield next;
await next();
}
}
function *all(next) {
yield random.call(this, backwards.call(this, pi.call(this, next)));
}
const all = compose([random, backwards, pi]);
app.use(all);
```
This is exactly what [koa-compose](https://github.com/koajs/compose) does, which Koa internally uses to create and dispatch the middleware stack.
### Response Middleware
Middleware that decide to respond to a request and wish to bypass downstream middleware may
simply omit `yield next`. Typically this will be in routing middleware, but this can be performed by
simply omit `next()`. Typically this will be in routing middleware, but this can be performed by
any. For example the following will respond with "two", however all three are executed, giving the
downstream "three" middleware a chance to manipulate the response.
```js
app.use(function *(next){
app.use(async function (ctx, next) {
console.log('>> one');
yield next;
await next();
console.log('<< one');
});
app.use(function *(next){
app.use(async function (ctx, next) {
console.log('>> two');
this.body = 'two';
yield next;
ctx.body = 'two';
await next();
console.log('<< two');
});
app.use(function *(next){
app.use(async function (ctx, next) {
console.log('>> three');
yield next;
await next();
console.log('<< three');
});
```
The following configuration omits `yield next` in the second middleware, and will still respond
The following configuration omits `next()` in the second middleware, and will still respond
with "two", however the third (and any other downstream middleware) will be ignored:
```js
app.use(function *(next){
app.use(async function (ctx, next) {
console.log('>> one');
yield next;
await next();
console.log('<< one');
});
app.use(function *(next){
app.use(async function (ctx, next) {
console.log('>> two');
this.body = 'two';
ctx.body = 'two';
console.log('<< two');
});
app.use(function *(next){
app.use(async function (ctx, next) {
console.log('>> three');
yield next;
await next();
console.log('<< three');
});
```
When the furthest downstream middleware executes `yield next;` it's really yielding to a noop
When the furthest downstream middleware executes `next();`, it's really yielding to a noop
function, allowing the middleware to compose correctly anywhere in the stack.
## Async operations
The [Co](https://github.com/visionmedia/co) forms Koa's foundation for generator delegation, allowing
Async function and promise forms Koa's foundation, allowing
you to write non-blocking sequential code. For example this middleware reads the filenames from `./docs`,
and then reads the contents of each markdown file in parallel before assigning the body to the joint result.
```js
var fs = require('co-fs');
const fs = require('mz/fs');
app.use(function *(){
var paths = yield fs.readdir('docs');
app.use(async function (ctx, next) {
const paths = await fs.readdir('docs');
const files = await Promise.all(paths.map(path => fs.readFile(`docs/${path}`, 'utf8')));
var files = yield paths.map(function(path){
return fs.readFile('docs/' + path, 'utf8');
});
this.type = 'markdown';
this.body = files.join('');
ctx.type = 'markdown';
ctx.body = files.join('');
});
```
## 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.
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.
```
$ DEBUG=koa* node --harmony examples/simple
@ -251,20 +226,20 @@ $ DEBUG=koa* node --harmony examples/simple
Since JavaScript does not allow defining function names at
runtime, you can also set a middleware's name as `._name`.
This useful when you don't have control of a middleware's name.
This is useful when you don't have control of a middleware's name.
For example:
```js
var path = require('path');
var static = require('koa-static');
const path = require('path');
const serve = require('koa-static');
var publicFiles = static(path.join(__dirname, 'public'));
const publicFiles = serve(path.join(__dirname, 'public'));
publicFiles._name = 'static /public';
app.use(publicFiles);
```
Now, instead of just seeing "static" when debugging, you will see:
Now, instead of just seeing "serve" when debugging, you will see:
```
koa:application use static /public +0ms

View File

@ -1,10 +1,8 @@
THIS DOCUMENT IS IN PROGRESS. THIS PARAGRAPH SHALL BE REMOVED WHEN THIS DOCUMENT IS DONE.
# Koa vs Express
Philosophically, Koa aims to "fix and replace node", whereas Express "augments node".
Koa uses co to rid apps of callback hell and simplify error handling.
It exposes its own `this.request` and `this.response` objects instead of node's `req` and `res` objects.
Koa uses promises and async functions to rid apps of callback hell and simplify error handling.
It exposes its own `ctx.request` and `ctx.response` objects instead of node's `req` and `res` objects.
Express, on the other hand, augments node's `req` and `res` objects with additional properties and methods
and includes many other "framework" features, such as routing and templating, which Koa does not.
@ -21,7 +19,7 @@ THIS DOCUMENT IS IN PROGRESS. THIS PARAGRAPH SHALL BE REMOVED WHEN THIS DOCUMENT
Thus, if you'd like to be closer to node.js and traditional node.js-style coding, you probably want to stick to Connect/Express or similar frameworks.
If you want to dive into the land of generators, use Koa.
If you want to get rid of callbacks, use Koa.
As result of this different philosophy is that traditional node.js "middleware", i.e. functions of the form `(req, res, next)`, are incompatible with Koa. Your application will essentially have to be rewritten from the ground, up.
@ -55,9 +53,7 @@ THIS DOCUMENT IS IN PROGRESS. THIS PARAGRAPH SHALL BE REMOVED WHEN THIS DOCUMENT
## How is Koa different than Connect/Express?
### Generated-based control flow
Thanks to co.
### Promises-based control flow
No callback hell.
@ -86,3 +82,12 @@ THIS DOCUMENT IS IN PROGRESS. THIS PARAGRAPH SHALL BE REMOVED WHEN THIS DOCUMENT
Better user experience.
Proper stream handling.
### Koa routing (third party libraries support)
Since Express comes with its own routing, but Koa does not have
any in-built routing, there are third party libraries available such as
koa-router and koa-route.
Similarly, just like we have helmet for security in Express, for Koa
we have koa-helmet available and the list goes on for Koa available third
party libraries.

BIN
docs/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 782 KiB

After

Width:  |  Height:  |  Size: 66 KiB

133
docs/migration.md Normal file
View File

@ -0,0 +1,133 @@
# Migrating from Koa v1.x to v2.x
## New middleware signature
Koa v2 introduces a new signature for middleware.
**Old signature middleware (v1.x) support will be removed in v3**
The new middleware signature is:
```js
// uses async arrow functions
app.use(async (ctx, next) => {
try {
await next() // next is now a function
} catch (err) {
ctx.body = { message: err.message }
ctx.status = err.status || 500
}
})
app.use(async ctx => {
const user = await User.getById(this.session.userid) // await instead of yield
ctx.body = user // ctx instead of this
})
```
You don't have to use asynchronous functions - you just have to pass a function that returns a promise.
A regular function that returns a promise works too!
The signature has changed to pass `Context` via an explicit parameter, `ctx` above, instead of via
`this`. The context passing change makes Koa more compatible with es6 arrow functions, which capture `this`.
## Using v1.x Middleware in v2.x
Koa v2.x will try to convert legacy signature, generator middleware on `app.use`, using [koa-convert](https://github.com/koajs/convert).
It is however recommended that you choose to migrate all v1.x middleware as soon as possible.
```js
// Koa will convert
app.use(function *(next) {
const start = Date.now();
yield next;
const ms = Date.now() - start;
console.log(`${this.method} ${this.url} - ${ms}ms`);
});
```
You could do it manually as well, in which case Koa will not convert.
```js
const convert = require('koa-convert');
app.use(convert(function *(next) {
const start = Date.now();
yield next;
const ms = Date.now() - start;
console.log(`${this.method} ${this.url} - ${ms}ms`);
}));
```
## Upgrading middleware
You will have to convert your generators to async functions with the new middleware signature:
```js
app.use(async (ctx, next) => {
const user = await Users.getById(this.session.user_id);
await next();
ctx.body = { message: 'some message' };
})
```
Upgrading your middleware may require some work. One migration path is to update them one-by-one.
1. Wrap all your current middleware in `koa-convert`
2. Test
3. `npm outdated` to see which Koa middleware is outdated
4. Update one outdated middleware, remove using `koa-convert`
5. Test
6. Repeat steps 3-5 until you're done
## Updating your code
You should start refactoring your code now to ease migrating to Koa v2:
- Return promises everywhere!
- Do not use `yield*`
- Do not use `yield {}` or `yield []`.
- Convert `yield []` into `yield Promise.all([])`
- Convert `yield {}` into `yield Bluebird.props({})`
You could also refactor your logic outside of Koa middleware functions. Create functions like
`function* someLogic(ctx) {}` and call it in your middleware as
`const result = yield someLogic(this)`.
Not using `this` will help migrations to the new middleware signature, which does not use `this`.
## Application object constructor requires new
In v1.x, the Application constructor function could be called directly, without `new` to
instantiate an instance of an application. For example:
```js
var koa = require('koa');
var app = module.exports = koa();
```
v2.x uses es6 classes which require the `new` keyword to be used.
```js
var koa = require('koa');
var app = module.exports = new koa();
```
## ENV specific logging behavior removed
An explicit check for the `test` environment was removed from error handling.
## Dependency changes
- [co](https://github.com/tj/co) is no longer bundled with Koa. Require or import it directly.
- [composition](https://github.com/thenables/composition) is no longer used and deprecated.
## v1.x support
The v1.x branch is still supported but should not receive feature updates. Except for this migration
guide, documentation will target the latest version.
## Help out
If you encounter migration related issues not covered by this migration guide, please consider
submitting a documentation pull request.

172
docs/troubleshooting.md Normal file
View File

@ -0,0 +1,172 @@
# Troubleshooting Koa
- [Whenever I try to access my route, it sends back a 404](#whenever-i-try-to-access-my-route-it-sends-back-a-404)
- [My response or context changes have no effect](#my-response-or-context-changes-have-no-effect)
- [My middleware is not called](#my-middleware-is-not-called)
See also [debugging Koa](guide.md#debugging-koa).
If you encounter a problem and later learn how to fix it, and think others might also encounter that problem, please
consider contributing to this documentation.
## Whenever I try to access my route, it sends back a 404
This is a common but troublesome problem when working with Koa middleware. First, it is critical to understand when Koa generates a 404. Koa does not care which or how much middleware was run, in many cases a 200 and 404 trigger the same number of middleware. Instead, the default status for any response is 404. The most obvious way this is changed is through `ctx.status`. However, if `ctx.body` is set when the status has not been explicitly defined (through `ctx.status`), the status is set to 200. This explains why simply setting the body results in a 200. Once the middleware is done (when the middleware and any returned promises are complete), Koa sends out the response. After that, nothing can alter the response. If it was a 404 at the time, it will be a 404 at the end, even if `ctx.status` or `ctx.body` are set afterwords.
Even though we now understand the basis of a 404, it might not be as clear why a 404 is generated in a specific case. This can be especially troublesome when it seems that `ctx.status` or `ctx.body` are set.
The unexpected 404 is a specific symptom of one of these more general problems:
- [My response or context changes have no effect](#my-response-or-context-changes-have-no-effect)
- [My middleware is not called](#my-middleware-is-not-called)
## My response or context changes have no effect
This can be caused when the response is sent before the code making the change is
executed. If the change is to the `ctx.body` or `ctx.status` setter, this can cause a 404 and
is by far the most common cause of these problems.
### Problematic code
```js
router.get('/fetch', function (ctx, next) {
models.Book.findById(parseInt(ctx.query.id)).then(function (book) {
ctx.body = book;
});
});
```
When used, this route will always send back a 404, even though `ctx.body` is set.
The same behavior would occur in this `async` version:
```js
router.get('/fetch', async (ctx, next) => {
models.Book.findById(parseInt(ctx.query.id)).then(function (book) {
ctx.body = book;
});
});
```
### Cause
`ctx.body` is not set until *after* the response has been sent. The code doesn't tell Koa to wait for the database to return the record. Koa sends the response after the middleware has been run, but not after the callback inside the middleware has been run. In the gap there, `ctx.body` has not yet been set, so Koa responds with a 404.
### Identifying this as the issue
Adding another piece of middleware and some logging can be extremely helpful in identifying this issue.
```js
router.use('/fetch', function (ctx, next) {
return next().then(function () {
console.log('Middleware done');
});
});
router.get('/fetch', function (ctx, next) {
models.Book.findById(parseInt(ctx.query.id)).then(function (book) {
ctx.body = book;
console.log('Body set');
});
});
```
If you see this in the logs:
```
Middleware done
Body set
```
It means that the body is being set after the middleware is done, and after the response has been sent. If you see only one or none of these logs, proceed to [My middleware is not called](#my-middleware-is-not-called). If they are in the right order, make sure you haven't explicitly set the status to 404, make sure that it actually is a 404, and if that fails feel free to ask for help.
### Solution
```js
router.get('/fetch', function (ctx, next) {
return models.Book.findById(parseInt(ctx.query.id)).then(function (book) {
ctx.body = book;
});
});
```
Returning the promise given by the database interface tells Koa to wait for the promise to finish before responding. At that time, the body will have been set. This results in Koa sending back a 200 with a proper response.
The fix in the `async` version is to add an `await` statement:
```js
router.get('/fetch', async (ctx, next) => {
await models.Book.findById(parseInt(ctx.query.id)).then(function (book) {
ctx.body = book;
});
});
```
## My middleware is not called
This can be due to an interrupted chain of middleware calls. This can cause a 404 if the
middleware that is skipped is responsible for the `ctx.body` or `ctx.status` setter.
This is less common than [My response or context changes have no effect](#my-response-or-context-changes-have-no-effect),
but it can be a much bigger pain to troubleshoot.
### Problematic code
```js
router.use(function (ctx, next) {
// Don't Repeat Yourself! Let's parse the ID here for all our middleware
if (ctx.query.id) {
ctx.state.id = parseInt(ctx.query.id);
}
});
router.get('/fetch', function (ctx, next) {
return models.Book.findById(ctx.state.id).then(function (book) {
ctx.body = book;
});
});
```
### Cause
In the code above, the book is never fetched from the database, and in fact our route was never called. Look closely at our helper middleware. We forgot to `return next()`! This causes the middleware flow to never reach our route, ending our "helper" middleware.
### Identifying this as the issue
Identifying this problem is easier than most, add a log at the beginning of the route. If it doesn't trigger, your route was never reached in the middleware chain.
```js
router.use(function (ctx, next) {
// Don't Repeat Yourself! Let's parse the ID here for all our middleware
if (ctx.query.id) {
ctx.state.id = parseInt(ctx.query.id);
}
});
router.get('/fetch', function (ctx, next) {
console.log('Route called'); // Never happens
return models.Book.findById(ctx.state.id).then(function (book) {
ctx.body = book;
});
});
```
To find the middleware causing the problem, try adding logging at various points in the middleware chain.
### Solution
The solution for this is rather easy, simply add `return next()` to the end of your helper middleware.
```js
router.use(function (ctx, next) {
if (ctx.query.id) {
ctx.state.id = parseInt(ctx.query.id);
}
return next();
});
router.get('/fetch', function (ctx, next) {
return models.Book.findById(ctx.state.id).then(function (book) {
ctx.body = book;
});
});
```

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

@ -1,210 +1,235 @@
'use strict';
/**
* Module dependencies.
*/
var debug = require('debug')('koa:application');
var Emitter = require('events').EventEmitter;
var compose_es7 = require('composition');
var onFinished = require('on-finished');
var response = require('./response');
var compose = require('koa-compose');
var isJSON = require('koa-is-json');
var context = require('./context');
var request = require('./request');
var statuses = require('statuses');
var Cookies = require('cookies');
var accepts = require('accepts');
var assert = require('assert');
var Stream = require('stream');
var http = require('http');
var only = require('only');
var co = require('co');
const debug = require('debug-ms')('koa:application');
const Emitter = require('events');
const util = require('util');
const Stream = require('stream');
const http = require('http');
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');
/**
* Application prototype.
* Expose `Application` class.
* Inherits from `Emitter.prototype`.
*/
var app = Application.prototype;
module.exports = class Application extends Emitter {
/**
* Initialize a new `Application`.
*
* @api public
*/
/**
* Expose `Application`.
*/
/**
*
* @param {object} [options] Application options
* @param {string} [options.env='development'] Environment
* @param {boolean} [options.proxy] Trust proxy headers
* @param {number} [options.subdomainOffset] Subdomain offset
*
*/
exports = module.exports = Application;
/**
* Initialize a new `Application`.
*
* @api public
*/
function Application() {
if (!(this instanceof Application)) return new Application;
this.env = process.env.NODE_ENV || 'development';
this.subdomainOffset = 2;
this.middleware = [];
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
/**
* Inherit from `Emitter.prototype`.
*/
Object.setPrototypeOf(Application.prototype, Emitter.prototype);
/**
* Shorthand for:
*
* http.createServer(app.callback()).listen(...)
*
* @param {Mixed} ...
* @return {Server}
* @api public
*/
app.listen = function(){
debug('listen');
var server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
};
/**
* Return JSON representation.
* We only bother showing settings.
*
* @return {Object}
* @api public
*/
app.inspect =
app.toJSON = function(){
return only(this, [
'subdomainOffset',
'env'
]);
};
/**
* Use the given middleware `fn`.
*
* @param {GeneratorFunction} fn
* @return {Application} self
* @api public
*/
app.use = function(fn){
if (!this.experimental) {
// es7 async functions are allowed
assert(fn && 'GeneratorFunction' == fn.constructor.name, 'app.use() requires a generator function');
constructor(options) {
super();
options = options || {};
this.proxy = options.proxy || false;
this.subdomainOffset = options.subdomainOffset || 2;
this.env = options.env || process.env.NODE_ENV || 'development';
this.middleware = [];
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
if (util.inspect.custom) {
this[util.inspect.custom] = this.inspect;
}
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
};
/**
* Return a request handler callback
* for node's native http server.
*
* @return {Function}
* @api public
*/
/**
* Shorthand for:
*
* http.createServer(app.callback()).listen(...)
*
* @param {Mixed} ...
* @return {Server}
* @api public
*/
app.callback = function(){
var mw = [respond].concat(this.middleware);
var fn = this.experimental
? compose_es7(mw)
: co.wrap(compose(mw));
var self = this;
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
if (!this.listeners('error').length) this.on('error', this.onerror);
/**
* Return JSON representation.
* We only bother showing settings.
*
* @return {Object}
* @api public
*/
return function(req, res){
toJSON() {
return {
subdomainOffset: this.subdomainOffset,
proxy: this.proxy,
env: this.env
};
}
/**
* Inspect implementation.
*
* @return {Object}
* @api public
*/
inspect() {
return this.toJSON();
}
/**
* Use the given middleware `fn`.
*
* Old-style middleware will be converted.
*
* @param {Function} fn
* @return {Application} self
* @api public
*/
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}
/**
* Return a request handler callback
* for node's native http server.
*
* @return {Function}
* @api public
*/
callback() {
const fn = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error', this.onerror);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
/**
* Handle request in callback.
*
* @api private
*/
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
var ctx = self.createContext(req, res);
onFinished(res, ctx.onerror);
fn.call(ctx).catch(ctx.onerror);
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
/**
* Initialize a new context.
*
* @api private
*/
createContext(req, res) {
const context = Object.create(this.context);
const request = context.request = Object.create(this.request);
const response = context.response = Object.create(this.response);
context.app = request.app = response.app = this;
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.originalUrl = request.originalUrl = req.url;
context.state = {};
return context;
}
/**
* Default error handler.
*
* @param {Error} err
* @api private
*/
onerror(err) {
if (!(err instanceof Error)) throw new TypeError(util.format('non-error thrown: %j', err));
if (404 == err.status || err.expose) return;
if (this.silent) return;
const msg = err.stack || err.toString();
console.error();
console.error(msg.replace(/^/gm, ' '));
console.error();
}
};
/**
* Initialize a new context.
*
* @api private
* Response helper.
*/
app.createContext = function(req, res){
var context = Object.create(this.context);
var request = context.request = Object.create(this.request);
var response = context.response = Object.create(this.response);
context.app = request.app = response.app = this;
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.onerror = context.onerror.bind(context);
context.originalUrl = request.originalUrl = req.url;
context.cookies = new Cookies(req, res, this.keys);
context.accept = request.accept = accepts(req);
context.state = {};
return context;
};
/**
* Default error handler.
*
* @param {Error} err
* @api private
*/
app.onerror = function(err){
assert(err instanceof Error, 'non-error thrown: ' + err);
if (404 == err.status) return;
if ('test' == this.env) return;
var msg = err.stack || err.toString();
console.error();
console.error(msg.replace(/^/gm, ' '));
console.error();
};
/**
* Response middleware.
*/
function *respond(next) {
yield *next;
function respond(ctx) {
// allow bypassing koa
if (false === this.respond) return;
if (false === ctx.respond) return;
var res = this.res;
if (res.headersSent || !this.writable) return;
if (!ctx.writable) return;
var body = this.body;
var code = this.status;
const res = ctx.res;
let body = ctx.body;
const code = ctx.status;
// ignore body
if (statuses.empty[code]) {
// strip headers
this.body = null;
ctx.body = null;
return res.end();
}
if ('HEAD' == this.method) {
if (isJSON(body)) this.length = Buffer.byteLength(JSON.stringify(body));
if ('HEAD' == ctx.method) {
if (!res.headersSent && isJSON(body)) {
ctx.length = Buffer.byteLength(JSON.stringify(body));
}
return res.end();
}
// status body
if (null == body) {
this.type = 'text';
body = this.message || String(code);
this.length = Buffer.byteLength(body);
if (ctx.req.httpVersionMajor >= 2) {
body = String(code);
} else {
body = ctx.message || String(code);
}
if (!res.headersSent) {
ctx.type = 'text';
ctx.length = Buffer.byteLength(body);
}
return res.end(body);
}
@ -215,6 +240,8 @@ function *respond(next) {
// body: json
body = JSON.stringify(body);
this.length = Buffer.byteLength(body);
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body);
}
res.end(body);
}

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

@ -1,18 +1,20 @@
'use strict';
/**
* Module dependencies.
*/
var createError = require('http-errors');
var httpAssert = require('http-assert');
var delegate = require('delegates');
var statuses = require('statuses');
const util = require('util');
const createError = require('http-errors-lite');
const delegate = require('./delegates');
const statuses = require('./statuses');
/**
* Context prototype.
*/
var proto = module.exports = {
const proto = module.exports = {
/**
* util.inspect() implementation, which
@ -22,7 +24,8 @@ var proto = module.exports = {
* @api public
*/
inspect: function(){
inspect() {
if (this === proto) return this;
return this.toJSON();
},
@ -38,7 +41,7 @@ var proto = module.exports = {
* @api public
*/
toJSON: function(){
toJSON() {
return {
request: this.request.toJSON(),
response: this.response.toJSON(),
@ -46,8 +49,8 @@ var proto = module.exports = {
originalUrl: this.originalUrl,
req: '<original node req>',
res: '<original node res>',
socket: '<original node socket>',
}
socket: '<original node socket>'
};
},
/**
@ -55,38 +58,41 @@ var 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 `msg` and optional `status`
* defaulting to 500. Note that these are user-level
* Throw an error with `status` (default 500) and
* `msg`. Note that these are user-level
* errors, and the message may be exposed to the client.
*
* this.throw(403)
* this.throw('name required', 400)
* this.throw(400, 'name required')
* this.throw('something exploded')
* this.throw(new Error('invalid'), 400);
* this.throw(400, new Error('invalid'));
* this.throw(new Error('invalid'))
* this.throw(400, new Error('invalid'))
*
* See: https://github.com/jshttp/http-errors
*
* Note: `status` should only be passed as the first parameter.
*
* @param {String|Number|Error} err, msg or status
* @param {String|Number|Error} [err, msg or status]
* @param {Object} [props]
* @api public
*/
throw: function(){
throw createError.apply(null, arguments);
throw(...args) {
throw createError(...args);
},
/**
@ -96,13 +102,18 @@ var proto = module.exports = {
* @api private
*/
onerror: function(err){
onerror(err) {
// 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('non-error thrown: ' + err);
if (!(err instanceof Error)) err = new Error(util.format('non-error thrown: %j', err));
let headerSent = false;
if (this.headerSent || !this.writable) {
headerSent = err.headerSent = true;
}
// delegate
this.app.emit('error', err, this);
@ -110,13 +121,22 @@ var proto = module.exports = {
// nothing we can do here other
// than delegate to the app-level
// handler and log.
if (this.headerSent || !this.writable) {
err.headerSent = true;
if (headerSent) {
return;
}
// unset all headers
this.res._headers = {};
const { res } = this;
// first unset all headers
/* istanbul ignore else */
if (typeof res.getHeaderNames === 'function') {
res.getHeaderNames().forEach(name => res.removeHeader(name));
} else {
res._headers = {}; // Node < 7.7
}
// then set those specified
this.set(err.headers);
// force text/plain
this.type = 'text';
@ -128,14 +148,26 @@ var proto = module.exports = {
if ('number' != typeof err.status || !statuses[err.status]) err.status = 500;
// respond
var code = statuses[err.status];
var msg = err.expose ? err.message : code;
const code = statuses[err.status];
const msg = err.expose ? err.message : code;
this.status = err.status;
this.length = Buffer.byteLength(msg);
this.res.end(msg);
res.end(msg);
}
};
/**
* Custom inspection implementation for newer Node.js versions.
*
* @return {Object}
* @api public
*/
/* istanbul ignore else */
if (util.inspect.custom) {
module.exports[util.inspect.custom] = module.exports.inspect;
}
/**
* Response delegation.
*/
@ -147,6 +179,7 @@ delegate(proto, 'response')
.method('vary')
.method('set')
.method('append')
.method('flushHeaders')
.access('status')
.access('message')
.access('body')
@ -176,11 +209,14 @@ delegate(proto, 'request')
.access('query')
.access('path')
.access('url')
.access('accept')
.getter('origin')
.getter('href')
.getter('subdomains')
.getter('protocol')
.getter('host')
.getter('hostname')
.getter('URL')
.getter('header')
.getter('headers')
.getter('secure')

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

@ -1,14 +1,20 @@
'use strict';
/**
* Module dependencies.
*/
var contentType = require('content-type');
var stringify = require('url').format;
var parse = require('parseurl');
var qs = require('querystring');
var typeis = require('type-is');
var fresh = require('fresh');
const URL = require('url').URL;
const net = require('net');
const stringify = require('url').format;
const qs = require('querystring');
const util = require('util');
const fresh = require('./fresh');
const fastparse = require('./fastparse');
const accepts = require('./accepts');
const IP = Symbol('context#ip');
/**
* Prototype.
@ -27,6 +33,16 @@ module.exports = {
return this.req.headers;
},
/**
* Set request header.
*
* @api public
*/
set header(val) {
this.req.headers = val;
},
/**
* Return request header, alias as request.header
*
@ -38,6 +54,16 @@ module.exports = {
return this.req.headers;
},
/**
* Set request header, alias as request.header
*
* @api public
*/
set headers(val) {
this.req.headers = val;
},
/**
* Get request URL.
*
@ -59,6 +85,17 @@ module.exports = {
this.req.url = val;
},
/**
* Get origin of URL.
*
* @return {String}
* @api public
*/
get origin() {
return `${this.protocol}://${this.host}`;
},
/**
* Get full request URL.
*
@ -68,10 +105,8 @@ module.exports = {
get href() {
// support: `GET http://example.com/foo`
if (/^https?:\/\//i.test(this.originalUrl)) {
return this.originalUrl;
}
return this.protocol + '://' + this.host + this.originalUrl;
if (/^https?:\/\//i.test(this.originalUrl)) return this.originalUrl;
return this.origin + this.originalUrl;
},
/**
@ -104,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;
},
/**
@ -115,7 +157,9 @@ module.exports = {
*/
set path(path) {
var url = parse(this.req);
const url = this.urlParsed;
if (url.pathname === path) return;
url.pathname = path;
url.path = null;
@ -130,8 +174,8 @@ module.exports = {
*/
get query() {
var str = this.querystring;
var c = this._querycache = this._querycache || {};
const str = this.querystring;
const c = this._querycache = this._querycache || {};
return c[str] || (c[str] = qs.parse(str));
},
@ -154,7 +198,8 @@ module.exports = {
*/
get querystring() {
return parse(this.req).query || '';
if (!this.req) return '';
return this.urlParsed.query || '';
},
/**
@ -165,7 +210,9 @@ module.exports = {
*/
set querystring(str) {
var url = parse(this.req);
const url = this.urlParsed;
if (url.search === `?${str}`) return;
url.search = str;
url.path = null;
@ -182,12 +229,12 @@ module.exports = {
get search() {
if (!this.querystring) return '';
return '?' + this.querystring;
return `?${this.querystring}`;
},
/**
* Set the search string. Same as
* response.querystring= but included for ubiquity.
* request.querystring= but included for ubiquity.
*
* @param {String} str
* @api public
@ -207,11 +254,14 @@ module.exports = {
*/
get host() {
var proxy = this.app.proxy;
var host = proxy && this.get('X-Forwarded-Host');
host = host || this.get('Host');
const proxy = this.app.proxy;
let host = proxy && this.get('X-Forwarded-Host');
if (!host) {
if (this.req.httpVersionMajor >= 2) host = this.get(':authority');
if (!host) host = this.get('Host');
}
if (!host) return '';
return host.split(/\s*,\s*/)[0];
return host.split(/\s*,\s*/, 1)[0];
},
/**
@ -224,9 +274,31 @@ module.exports = {
*/
get hostname() {
var host = this.host;
const host = this.host;
if (!host) return '';
return host.split(':')[0];
if ('[' == host[0]) return this.URL.hostname || ''; // IPv6
return host.split(':', 1)[0];
},
/**
* Get WHATWG parsed URL.
* Lazily memoized.
*
* @return {URL|Object}
* @api public
*/
get URL() {
/* istanbul ignore else */
if (!this.memoizedURL) {
const originalUrl = this.originalUrl || ''; // avoid undefined in template string
try {
this.memoizedURL = new URL(`${this.origin}${originalUrl}`);
} catch (err) {
this.memoizedURL = Object.create(null);
}
}
return this.memoizedURL;
},
/**
@ -239,15 +311,15 @@ module.exports = {
*/
get fresh() {
var method = this.method;
var s = this.ctx.status;
const method = this.method;
const s = this.ctx.status;
// GET or HEAD for weak freshness validation only
if ('GET' != method && 'HEAD' != method) return false;
// 2xx or 304 as per rfc2616 14.26
if ((s >= 200 && s < 300) || 304 == s) {
return fresh(this.header, this.ctx.response.header);
return fresh(this.header, this.response.header);
}
return false;
@ -274,7 +346,7 @@ module.exports = {
*/
get idempotent() {
var methods = ['GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS', 'TRACE'];
const methods = ['GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS', 'TRACE'];
return !!~methods.indexOf(this.method);
},
@ -286,24 +358,9 @@ module.exports = {
*/
get socket() {
// TODO: TLS
return this.req.socket;
},
/**
* Get the charset when present or undefined.
*
* @return {String}
* @api public
*/
get charset() {
var type = this.get('Content-Type');
if (!type) return '';
return contentType.parse(type).parameters.charset || '';
},
/**
* Return parsed Content-Length when present.
*
@ -312,7 +369,7 @@ module.exports = {
*/
get length() {
var len = this.get('Content-Length');
const len = this.get('Content-Length');
if (len == '') return;
return ~~len;
},
@ -330,11 +387,10 @@ module.exports = {
*/
get protocol() {
var proxy = this.app.proxy;
if (this.socket.encrypted) return 'https';
if (!proxy) return 'http';
var proto = this.get('X-Forwarded-Proto') || 'http';
return proto.split(/\s*,\s*/)[0];
if (!this.app.proxy) return 'http';
const proto = this.get('X-Forwarded-Proto');
return proto ? proto.split(/\s*,\s*/, 1)[0] : 'http';
},
/**
@ -350,19 +406,6 @@ module.exports = {
return 'https' == this.protocol;
},
/**
* Return the remote address, or when
* `app.proxy` is `true` return
* the upstream addr.
*
* @return {String}
* @api public
*/
get ip() {
return this.ips[0] || this.socket.remoteAddress || '';
},
/**
* When `app.proxy` is `true`, parse
* the "X-Forwarded-For" ip address list.
@ -376,22 +419,43 @@ module.exports = {
*/
get ips() {
var proxy = this.app.proxy;
var val = this.get('X-Forwarded-For');
const proxy = this.app.proxy;
const val = this.get('X-Forwarded-For');
return proxy && val
? val.split(/\s*,\s*/)
: [];
},
/**
* Return request's remote address
* When `app.proxy` is `true`, parse
* the "X-Forwarded-For" ip address list and return the first one
*
* @return {String}
* @api public
*/
get ip() {
if (!this[IP]) {
this[IP] = this.ips[0] || this.socket.remoteAddress || '';
}
return this[IP];
},
set ip(_ip) {
this[IP] = _ip;
},
/**
* Return subdomains as an array.
*
* Subdomains are the dot-separated parts of the host before the main domain of
* the app. By default, the domain of the app is assumed to be the last two
* Subdomains are the dot-separated parts of the host before the main domain
* of the app. By default, the domain of the app is assumed to be the last two
* parts of the host. This can be changed by setting `app.subdomainOffset`.
*
* For example, if the domain is "tobi.ferrets.example.com":
* If `app.subdomainOffset` is not set, this.subdomains is `["ferrets", "tobi"]`.
* If `app.subdomainOffset` is not set, this.subdomains is
* `["ferrets", "tobi"]`.
* If `app.subdomainOffset` is 3, this.subdomains is `["tobi"]`.
*
* @return {Array}
@ -399,8 +463,10 @@ module.exports = {
*/
get subdomains() {
var offset = this.app.subdomainOffset;
return (this.host || '')
const offset = this.app.subdomainOffset;
const hostname = this.hostname;
if (net.isIP(hostname)) return [];
return hostname
.split('.')
.reverse()
.slice(offset);
@ -408,7 +474,7 @@ module.exports = {
/**
* Check if the given `type(s)` is acceptable, returning
* the best match when true, otherwise `undefined`, in which
* the best match when true, otherwise `false`, in which
* case you should respond with 406 "Not Acceptable".
*
* The `type` value may be a single mime type string
@ -435,7 +501,7 @@ module.exports = {
* // Accept: text/*, application/json
* this.accepts('image/png');
* this.accepts('png');
* // => undefined
* // => false
*
* // Accept: text/*;q=.5, application/json
* this.accepts(['html', 'json']);
@ -443,12 +509,19 @@ module.exports = {
* // => "json"
*
* @param {String|Array} type(s)...
* @return {String|Array|Boolean}
* @return {String|Array|false}
* @api public
*/
accepts: function(){
return this.accept.types.apply(this.accept, arguments);
accepts(...args) {
let types = [...args];
// If passed an array, grab it
if (types[0] instanceof Array) {
types = types[0];
}
return accepts(this, 'accept', types);
},
/**
@ -464,8 +537,15 @@ module.exports = {
* @api public
*/
acceptsEncodings: function(){
return this.accept.encodings.apply(this.accept, arguments);
acceptsEncodings(...args) {
let types = [...args];
// If passed an array, grab it
if (types[0] instanceof Array) {
types = types[0];
}
return accepts(this, 'accept-encoding', types);
},
/**
@ -481,8 +561,15 @@ module.exports = {
* @api public
*/
acceptsCharsets: function(){
return this.accept.charsets.apply(this.accept, arguments);
acceptsCharsets(...args) {
let types = [...args];
// If passed an array, grab it
if (types[0] instanceof Array) {
types = types[0];
}
return accepts(this, 'accept-charset', types);
},
/**
@ -498,8 +585,15 @@ module.exports = {
* @api public
*/
acceptsLanguages: function(){
return this.accept.languages.apply(this.accept, arguments);
acceptsLanguages(...args) {
let types = [...args];
// If passed an array, grab it
if (types[0] instanceof Array) {
types = types[0];
}
return accepts(this, 'accept-language', types);
},
/**
@ -528,10 +622,10 @@ module.exports = {
* @api public
*/
is: function(types){
if (!types) return typeis(this.req);
is(types) {
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);
},
/**
@ -543,7 +637,7 @@ module.exports = {
*/
get type() {
var type = this.get('Content-Type');
const type = this.get('Content-Type');
if (!type) return '';
return type.split(';')[0];
},
@ -563,15 +657,15 @@ module.exports = {
* // => "text/plain"
*
* this.get('Something');
* // => undefined
* // => ''
*
* @param {String} field
* @return {String}
* @api public
*/
get: function(field){
var req = this.req;
get(field) {
const req = this.req;
switch (field = field.toLowerCase()) {
case 'referer':
case 'referrer':
@ -588,7 +682,7 @@ module.exports = {
* @api public
*/
inspect: function(){
inspect() {
if (!this.req) return;
return this.toJSON();
},
@ -600,7 +694,7 @@ module.exports = {
* @api public
*/
toJSON: function(){
toJSON() {
return {
method: this.method,
url: this.url,
@ -608,3 +702,15 @@ module.exports = {
};
}
};
/**
* Custom inspection implementation for newer Node.js versions.
*
* @return {Object}
* @api public
*/
/* istanbul ignore else */
if (util.inspect.custom) {
module.exports[util.inspect.custom] = module.exports.inspect;
}

View File

@ -1,21 +1,20 @@
'use strict';
/**
* Module dependencies.
*/
var contentDisposition = require('content-disposition');
var ensureErrorHandler = require('error-inject');
var getType = require('mime-types').contentType;
var onFinish = require('on-finished');
var isJSON = require('koa-is-json');
var escape = require('escape-html');
var typeis = require('type-is').is;
var statuses = require('statuses');
var destroy = require('destroy');
var assert = require('assert');
var path = require('path');
var vary = require('vary');
var extname = path.extname;
const ReadStream = require('fs').ReadStream;
const contentDisposition = require('./content-disposition');
const assert = require('assert');
const extname = require('path').extname;
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.
@ -31,8 +30,7 @@ module.exports = {
*/
get socket() {
// TODO: TLS
return this.ctx.req.socket;
return this.res.socket;
},
/**
@ -43,8 +41,10 @@ module.exports = {
*/
get header() {
// TODO: wtf
return this.res._headers || {};
const { res } = this;
return typeof res.getHeaders === 'function'
? res.getHeaders()
: res._headers || {}; // Node < 7.7
},
/**
@ -77,11 +77,13 @@ module.exports = {
*/
set status(code) {
assert('number' == typeof code, 'status code must be a number');
assert(statuses[code], 'invalid status code: ' + code);
if (this.headerSent) return;
assert(Number.isInteger(code), 'status code must be a number');
assert(code >= 100 && code <= 999, `invalid status code: ${code}`);
this._explicitStatus = true;
this.res.statusCode = code;
this.res.statusMessage = statuses[code];
if (this.req.httpVersionMajor < 2) this.res.statusMessage = statuses[code];
if (this.body && statuses.empty[code]) this.body = null;
},
@ -126,15 +128,15 @@ module.exports = {
*/
set body(val) {
var original = this._body;
const original = this._body;
this._body = val;
// no content
if (null == val) {
if (!statuses.empty[this.status]) this.status = 204;
this.res.removeHeader('Content-Type');
this.res.removeHeader('Content-Length');
this.res.removeHeader('Transfer-Encoding');
this.remove('Content-Type');
this.remove('Content-Length');
this.remove('Transfer-Encoding');
return;
}
@ -142,7 +144,7 @@ module.exports = {
if (!this._explicitStatus) this.status = 200;
// set the content-type only if not yet set
var setType = !this.header['content-type'];
const setType = !this.header['content-type'];
// string
if ('string' == typeof val) {
@ -160,8 +162,25 @@ module.exports = {
// stream
if ('function' == typeof val.pipe) {
onFinish(this.res, destroy.bind(null, val));
ensureErrorHandler(val, this.ctx.onerror);
// 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');
@ -194,8 +213,8 @@ module.exports = {
*/
get length() {
var len = this.header['content-length'];
var body = this.body;
const len = this.header['content-length'];
const body = this.body;
if (null == len) {
if (!body) return;
@ -205,7 +224,7 @@ module.exports = {
return;
}
return ~~len;
return Math.trunc(len) || 0;
},
/**
@ -226,8 +245,17 @@ module.exports = {
* @api public
*/
vary: function(field){
vary(this.res, field);
vary(field) {
if (this.headerSent) return;
// 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(', '));
},
/**
@ -245,29 +273,35 @@ module.exports = {
* this.redirect('http://google.com');
*
* @param {String} url
* @param {String} alt
* @param {String} [alt]
* @api public
*/
redirect: function(url, alt){
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>.';
this.body = `Redirecting to <a href="${url}">${url}</a>.`;
return;
}
// text
this.type = 'text/plain; charset=utf-8';
this.body = 'Redirecting to ' + url + '.';
this.body = `Redirecting to ${url}.`;
},
/**
@ -277,9 +311,9 @@ module.exports = {
* @api public
*/
attachment: function(filename){
attachment(filename, options) {
if (filename) this.type = extname(filename);
this.set('Content-Disposition', contentDisposition(filename));
this.set('Content-Disposition', contentDisposition(filename, options));
},
/**
@ -298,8 +332,29 @@ module.exports = {
* @api public
*/
set type(type) {
this.set('Content-Type', getType(type) || 'application/octet-stream');
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 {
let mimetype = getMimetype(type, true);
if (mimetype) {
this.set('Content-Type', mimetype);
} else {
this.remove('Content-Type');
}
}
},
/**
@ -325,7 +380,7 @@ module.exports = {
*/
get lastModified() {
var date = this.get('last-modified');
const date = this.get('last-modified');
if (date) return new Date(date);
},
@ -342,7 +397,7 @@ module.exports = {
*/
set etag(val) {
if (!/^(W\/)?"/.test(val)) val = '"' + val + '"';
if (!/^(W\/)?"/.test(val)) val = `"${val}"`;
this.set('ETag', val);
},
@ -358,7 +413,7 @@ module.exports = {
},
/**
* Return the request mime type void of
* Return the response mime type void of
* parameters such as "charset".
*
* @return {String}
@ -366,9 +421,9 @@ module.exports = {
*/
get type() {
var type = this.get('Content-Type');
const type = this.get('Content-Type');
if (!type) return '';
return type.split(';')[0];
return type.split(';', 1)[0];
},
/**
@ -380,11 +435,10 @@ module.exports = {
* @api public
*/
is: function(types){
var type = this.type;
if (!types) return type || false;
is(types) {
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);
},
/**
@ -403,7 +457,7 @@ module.exports = {
* @api public
*/
get: function(field){
get(field) {
return this.header[field.toLowerCase()] || '';
},
@ -422,13 +476,15 @@ module.exports = {
* @api public
*/
set: function(field, val){
set(field, val) {
if (this.headerSent) return;
if (2 == arguments.length) {
if (Array.isArray(val)) val = val.map(String);
else val = String(val);
if (Array.isArray(val)) val = val.map(v => typeof v === 'string' ? v : String(v));
else if (typeof val !== 'string') val = String(val);
this.res.setHeader(field, val);
} else {
for (var key in field) {
for (const key in field) {
this.set(key, field[key]);
}
}
@ -439,17 +495,19 @@ module.exports = {
*
* Examples:
*
* this.append('Link', ['<http://localhost/>', '<http://localhost:3000/>']);
* this.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly');
* this.append('Warning', '199 Miscellaneous warning');
* ```
* this.append('Link', ['<http://localhost/>', '<http://localhost:3000/>']);
* this.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly');
* this.append('Warning', '199 Miscellaneous warning');
* ```
*
* @param {String} field
* @param {String|Array} val
* @api public
*/
append: function(field, val){
var prev = this.get(field);
append(field, val) {
const prev = this.get(field);
if (prev) {
val = Array.isArray(prev)
@ -467,7 +525,9 @@ module.exports = {
* @api public
*/
remove: function(field){
remove(field) {
if (this.headerSent) return;
this.res.removeHeader(field);
},
@ -481,8 +541,13 @@ module.exports = {
*/
get writable() {
var socket = this.res.socket;
if (!socket) return false;
// can't write any more after response finished
if (this.res.finished) return false;
const socket = this.res.socket;
// There are already pending outgoing res, but still writable
// https://github.com/nodejs/node/blob/v4.4.7/lib/_http_server.js#L486
if (!socket) return true;
return socket.writable;
},
@ -493,9 +558,9 @@ module.exports = {
* @api public
*/
inspect: function(){
inspect() {
if (!this.res) return;
var o = this.toJSON();
const o = this.toJSON();
o.body = this.body;
return o;
},
@ -507,11 +572,28 @@ module.exports = {
* @api public
*/
toJSON: function(){
toJSON() {
return {
status: this.status,
message: this.message,
header: this.header
};
},
/**
* Flush any set headers, and begin the body
*/
flushHeaders() {
this.res.flushHeaders();
}
};
/**
* Custom inspection implementation for newer Node.js versions.
*
* @return {Object}
* @api public
*/
if (util.inspect.custom) {
module.exports[util.inspect.custom] = module.exports.inspect;
}

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,12 +1,16 @@
{
"name": "koa",
"version": "0.21.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": "make test"
"test": "egg-bin test test",
"test-cov": "egg-bin cov test",
"lint": "eslint benchmarks lib test",
"bench": "make -C benchmarks",
"authors": "git log --format='%aN <%aE>' | sort -u > AUTHORS"
},
"repository": "koajs/koa",
"repository": "nfp-projects/koa-lite",
"keywords": [
"web",
"app",
@ -18,42 +22,21 @@
],
"license": "MIT",
"dependencies": {
"accepts": "^1.2.2",
"co": "^4.4.0",
"composition": "^2.1.1",
"content-disposition": "~0.5.0",
"content-type": "^1.0.0",
"cookies": "~0.5.0",
"debug": "*",
"delegates": "0.1.0",
"destroy": "^1.0.3",
"error-inject": "~1.0.0",
"escape-html": "~1.0.1",
"fresh": "^0.3.0",
"http-assert": "^1.1.0",
"http-errors": "^1.2.8",
"koa-compose": "^2.3.0",
"koa-is-json": "^1.0.0",
"mime-types": "^2.0.7",
"on-finished": "^2.1.0",
"only": "0.0.2",
"parseurl": "^1.3.0",
"statuses": "^1.2.0",
"type-is": "^1.5.5",
"vary": "^1.0.0"
"debug-ms": "~4.1.2",
"http-errors-lite": "^2.0.2"
},
"devDependencies": {
"babel": "^5.0.0",
"istanbul-harmony": "~0.3.0",
"make-lint": "^1.0.1",
"mocha": "^2.0.1",
"should": "^3.1.0",
"supertest": "~0.15.0",
"test-console": "^0.7.1"
"egg-bin": "^4.13.0",
"eslint": "^6.0.1",
"eslint-config-koa": "^2.0.0",
"eslint-config-standard": "^7.0.1",
"eslint-plugin-promise": "^3.5.0",
"eslint-plugin-standard": "^2.1.1",
"mm": "^2.5.0",
"supertest": "^3.1.0"
},
"engines": {
"node": ">= 0.11.16",
"iojs": ">= 1.0.0"
"node": "^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4"
},
"files": [
"lib"

6
test/.eslintrc Normal file
View File

@ -0,0 +1,6 @@
env:
jest: true
rules:
space-before-blocks: [2, {functions: never, keywords: always}]
no-unused-expressions: 0

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,34 @@
'use strict';
const request = require('supertest');
const assert = require('assert');
const Koa = require('../..');
describe('app.context', () => {
const app1 = new Koa();
app1.context.msg = 'hello';
const app2 = new Koa();
it('should merge properties', () => {
app1.use((ctx, next) => {
assert.equal(ctx.msg, 'hello');
ctx.status = 204;
});
return request(app1.listen())
.get('/')
.expect(204);
});
it('should not affect the original prototype', () => {
app2.use((ctx, next) => {
assert.equal(ctx.msg, undefined);
ctx.status = 204;
});
return request(app2.listen())
.get('/')
.expect(204);
});
});

73
test/application/index.js Normal file
View File

@ -0,0 +1,73 @@
'use strict';
const request = require('supertest');
const assert = require('assert');
const Koa = require('../..');
describe('app', () => {
it('should handle socket errors', done => {
const app = new Koa();
app.use((ctx, next) => {
// triggers ctx.socket.writable == false
ctx.socket.emit('error', new Error('boom'));
});
app.on('error', err => {
assert.equal(err.message, 'boom');
done();
});
request(app.callback())
.get('/')
.end(() => {});
});
it('should not .writeHead when !socket.writable', done => {
const app = new Koa();
app.use((ctx, next) => {
// set .writable to false
ctx.socket.writable = false;
ctx.status = 204;
// throw if .writeHead or .end is called
ctx.res.writeHead = ctx.res.end = () => {
throw new Error('response sent');
};
});
// hackish, but the response should occur in a single tick
setImmediate(done);
request(app.callback())
.get('/')
.end(() => {});
});
it('should set development env when NODE_ENV missing', () => {
const NODE_ENV = process.env.NODE_ENV;
process.env.NODE_ENV = '';
const app = new Koa();
process.env.NODE_ENV = NODE_ENV;
assert.equal(app.env, 'development');
});
it('should set env from the constructor', () => {
const env = 'custom';
const app = new Koa({ env });
assert.strictEqual(app.env, env);
});
it('should set proxy flag from the constructor', () => {
const proxy = true;
const app = new Koa({ proxy });
assert.strictEqual(app.proxy, proxy);
});
it('should set subdomainOffset from the constructor', () => {
const subdomainOffset = 3;
const app = new Koa({ subdomainOffset });
assert.strictEqual(app.subdomainOffset, subdomainOffset);
});
});

View File

@ -0,0 +1,21 @@
'use strict';
const assert = require('assert');
const util = require('util');
const Koa = require('../..');
const app = new Koa();
describe('app.inspect()', () => {
it('should work', () => {
const str = util.inspect(app);
assert.equal("{ subdomainOffset: 2, proxy: false, env: 'test' }", str);
});
it('should return a json representation', () => {
assert.deepEqual(
{ subdomainOffset: 2, proxy: false, env: 'test' },
app.inspect()
);
});
});

View File

@ -0,0 +1,73 @@
'use strict';
const assert = require('assert');
const Koa = require('../..');
const mm = require('mm');
describe('app.onerror(err)', () => {
afterEach(mm.restore);
it('should throw an error if a non-error is given', () => {
const app = new Koa();
assert.throws(() => {
app.onerror('foo');
}, TypeError, 'non-error thrown: foo');
});
it('should do nothing if status is 404', () => {
const app = new Koa();
const err = new Error();
err.status = 404;
let called = false;
mm(console, 'error', () => { called = true; });
app.onerror(err);
assert(!called);
});
it('should do nothing if .silent', () => {
const app = new Koa();
app.silent = true;
const err = new Error();
let called = false;
mm(console, 'error', () => { called = true; });
app.onerror(err);
assert(!called);
});
it('should log the error to stderr', () => {
const app = new Koa();
app.env = 'dev';
const err = new Error();
err.stack = 'Foo';
let msg = '';
mm(console, 'error', input => {
if (input) msg = input;
});
app.onerror(err);
assert(msg === ' Foo');
});
it('should use err.toString() instad of err.stack', () => {
const app = new Koa();
app.env = 'dev';
const err = new Error('mock stack null');
err.stack = null;
app.onerror(err);
let msg = '';
mm(console, 'error', input => {
if (input) msg = input;
});
app.onerror(err);
assert(msg === ' Error: mock stack null');
});
});

View File

@ -0,0 +1,34 @@
'use strict';
const request = require('supertest');
const assert = require('assert');
const Koa = require('../..');
describe('app.request', () => {
const app1 = new Koa();
app1.request.message = 'hello';
const app2 = new Koa();
it('should merge properties', () => {
app1.use((ctx, next) => {
assert.equal(ctx.request.message, 'hello');
ctx.status = 204;
});
return request(app1.listen())
.get('/')
.expect(204);
});
it('should not affect the original prototype', () => {
app2.use((ctx, next) => {
assert.equal(ctx.request.message, undefined);
ctx.status = 204;
});
return request(app2.listen())
.get('/')
.expect(204);
});
});

772
test/application/respond.js Normal file
View File

@ -0,0 +1,772 @@
'use strict';
const request = require('supertest');
const assert = require('assert');
const Koa = require('../..');
const fs = require('fs');
describe('app.respond', () => {
describe('when ctx.respond === false', () => {
it('should function (ctx)', () => {
const app = new Koa();
app.use(ctx => {
ctx.body = 'Hello';
ctx.respond = false;
const res = ctx.res;
res.statusCode = 200;
setImmediate(() => {
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Length', '3');
res.end('lol');
});
});
const server = app.listen();
return request(server)
.get('/')
.expect(200)
.expect('lol');
});
it('should ignore set header after header sent', () => {
const app = new Koa();
app.use(ctx => {
ctx.body = 'Hello';
ctx.respond = false;
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');
});
const server = app.listen();
return request(server)
.get('/')
.expect(200)
.expect('lol')
.expect(res => {
assert(!res.headers.foo);
});
});
it('should ignore set status after header sent', () => {
const app = new Koa();
app.use(ctx => {
ctx.body = 'Hello';
ctx.respond = false;
const res = ctx.res;
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Length', '3');
res.end('lol');
ctx.status = 201;
});
const server = app.listen();
return request(server)
.get('/')
.expect(200)
.expect('lol');
});
});
describe('when this.type === null', () => {
it('should not send Content-Type header', async() => {
const app = new Koa();
app.use(ctx => {
ctx.body = '';
ctx.type = null;
});
const server = app.listen();
const res = await request(server)
.get('/')
.expect(200);
assert.equal(res.headers.hasOwnProperty('Content-Type'), false);
});
});
describe('when HEAD is used', () => {
it('should not respond with the body', async() => {
const app = new Koa();
app.use(ctx => {
ctx.body = 'Hello';
});
const server = app.listen();
const res = await request(server)
.head('/')
.expect(200);
assert.equal(res.headers['content-type'], 'text/plain; charset=utf-8');
assert.equal(res.headers['content-length'], '5');
assert(!res.text);
});
it('should keep json headers', async() => {
const app = new Koa();
app.use(ctx => {
ctx.body = { hello: 'world' };
});
const server = app.listen();
const res = await request(server)
.head('/')
.expect(200);
assert.equal(res.headers['content-type'], 'application/json; charset=utf-8');
assert.equal(res.headers['content-length'], '17');
assert(!res.text);
});
it('should keep string headers', async() => {
const app = new Koa();
app.use(ctx => {
ctx.body = 'hello world';
});
const server = app.listen();
const res = await request(server)
.head('/')
.expect(200);
assert.equal(res.headers['content-type'], 'text/plain; charset=utf-8');
assert.equal(res.headers['content-length'], '11');
assert(!res.text);
});
it('should keep buffer headers', async() => {
const app = new Koa();
app.use(ctx => {
ctx.body = Buffer.from('hello world');
});
const server = app.listen();
const res = await request(server)
.head('/')
.expect(200);
assert.equal(res.headers['content-type'], 'application/octet-stream');
assert.equal(res.headers['content-length'], '11');
assert(!res.text);
});
it('should respond with a 404 if no body was set', () => {
const app = new Koa();
app.use(ctx => {
});
const server = app.listen();
return request(server)
.head('/')
.expect(404);
});
it('should respond with a 200 if body = ""', () => {
const app = new Koa();
app.use(ctx => {
ctx.body = '';
});
const server = app.listen();
return request(server)
.head('/')
.expect(200);
});
it('should not overwrite the content-type', () => {
const app = new Koa();
app.use(ctx => {
ctx.status = 200;
ctx.type = 'application/javascript';
});
const server = app.listen();
return request(server)
.head('/')
.expect('content-type', /application\/javascript/)
.expect(200);
});
});
describe('when no middleware are present', () => {
it('should 404', () => {
const app = new Koa();
const server = app.listen();
return request(server)
.get('/')
.expect(404);
});
});
describe('when res has already been written to', () => {
it('should not cause an app error', () => {
const app = new Koa();
app.use((ctx, next) => {
const res = ctx.res;
ctx.status = 200;
res.setHeader('Content-Type', 'text/html');
res.write('Hello');
});
app.on('error', err => { throw err; });
const server = app.listen();
return request(server)
.get('/')
.expect(200);
});
it('should send the right body', () => {
const app = new Koa();
app.use((ctx, next) => {
const res = ctx.res;
ctx.status = 200;
res.setHeader('Content-Type', 'text/html');
res.write('Hello');
return new Promise(resolve => {
setTimeout(() => {
res.end('Goodbye');
resolve();
}, 0);
});
});
const server = app.listen();
return request(server)
.get('/')
.expect(200)
.expect('HelloGoodbye');
});
});
describe('when .body is missing', () => {
describe('with status=400', () => {
it('should respond with the associated status message', () => {
const app = new Koa();
app.use(ctx => {
ctx.status = 400;
});
const server = app.listen();
return request(server)
.get('/')
.expect(400)
.expect('Content-Length', '11')
.expect('Bad Request');
});
});
describe('with status=204', () => {
it('should respond without a body', async() => {
const app = new Koa();
app.use(ctx => {
ctx.status = 204;
});
const server = app.listen();
const res = await request(server)
.get('/')
.expect(204)
.expect('');
assert.equal(res.headers.hasOwnProperty('content-type'), false);
});
});
describe('with status=205', () => {
it('should respond without a body', async() => {
const app = new Koa();
app.use(ctx => {
ctx.status = 205;
});
const server = app.listen();
const res = await request(server)
.get('/')
.expect(205)
.expect('');
assert.equal(res.headers.hasOwnProperty('content-type'), false);
});
});
describe('with status=304', () => {
it('should respond without a body', async() => {
const app = new Koa();
app.use(ctx => {
ctx.status = 304;
});
const server = app.listen();
const res = await request(server)
.get('/')
.expect(304)
.expect('');
assert.equal(res.headers.hasOwnProperty('content-type'), false);
});
});
describe('with custom statusMessage=ok', () => {
it('should respond with the custom status message', async() => {
const app = new Koa();
app.use(ctx => {
ctx.status = 200;
ctx.message = 'ok';
});
const server = app.listen();
const res = await request(server)
.get('/')
.expect(200)
.expect('ok');
assert.equal(res.res.statusMessage, 'ok');
});
});
describe('with custom status without message', () => {
it('should respond with the status code number', () => {
const app = new Koa();
app.use(ctx => {
ctx.res.statusCode = 701;
});
const server = app.listen();
return request(server)
.get('/')
.expect(701)
.expect('701');
});
});
});
describe('when .body is a null', () => {
it('should respond 204 by default', async() => {
const app = new Koa();
app.use(ctx => {
ctx.body = null;
});
const server = app.listen();
const res = await request(server)
.get('/')
.expect(204)
.expect('');
assert.equal(res.headers.hasOwnProperty('content-type'), false);
});
it('should respond 204 with status=200', async() => {
const app = new Koa();
app.use(ctx => {
ctx.status = 200;
ctx.body = null;
});
const server = app.listen();
const res = await request(server)
.get('/')
.expect(204)
.expect('');
assert.equal(res.headers.hasOwnProperty('content-type'), false);
});
it('should respond 205 with status=205', async() => {
const app = new Koa();
app.use(ctx => {
ctx.status = 205;
ctx.body = null;
});
const server = app.listen();
const res = await request(server)
.get('/')
.expect(205)
.expect('');
assert.equal(res.headers.hasOwnProperty('content-type'), false);
});
it('should respond 304 with status=304', async() => {
const app = new Koa();
app.use(ctx => {
ctx.status = 304;
ctx.body = null;
});
const server = app.listen();
const res = await request(server)
.get('/')
.expect(304)
.expect('');
assert.equal(res.headers.hasOwnProperty('content-type'), false);
});
});
describe('when .body is a string', () => {
it('should respond', () => {
const app = new Koa();
app.use(ctx => {
ctx.body = 'Hello';
});
const server = app.listen();
return request(server)
.get('/')
.expect('Hello');
});
});
describe('when .body is a Buffer', () => {
it('should respond', () => {
const app = new Koa();
app.use(ctx => {
ctx.body = Buffer.from('Hello');
});
const server = app.listen();
return request(server)
.get('/')
.expect(200)
.expect(Buffer.from('Hello'));
});
});
describe('when .body is a Stream', () => {
it('should respond', async() => {
const app = new Koa();
app.use(ctx => {
ctx.body = fs.createReadStream('package.json');
ctx.set('Content-Type', 'application/json; charset=utf-8');
});
const server = app.listen();
const res = await request(server)
.get('/')
.expect('Content-Type', 'application/json; charset=utf-8');
const pkg = require('../../package');
assert.equal(res.headers.hasOwnProperty('content-length'), false);
assert.deepEqual(res.body, pkg);
});
it('should strip content-length when overwriting', async() => {
const app = new Koa();
app.use(ctx => {
ctx.body = 'hello';
ctx.body = fs.createReadStream('package.json');
ctx.set('Content-Type', 'application/json; charset=utf-8');
});
const server = app.listen();
const res = await request(server)
.get('/')
.expect('Content-Type', 'application/json; charset=utf-8');
const pkg = require('../../package');
assert.equal(res.headers.hasOwnProperty('content-length'), false);
assert.deepEqual(res.body, pkg);
});
it('should keep content-length if not overwritten', async() => {
const app = new Koa();
app.use(ctx => {
ctx.length = fs.readFileSync('package.json').length;
ctx.body = fs.createReadStream('package.json');
ctx.set('Content-Type', 'application/json; charset=utf-8');
});
const server = app.listen();
const res = await request(server)
.get('/')
.expect('Content-Type', 'application/json; charset=utf-8');
const pkg = require('../../package');
assert.equal(res.headers.hasOwnProperty('content-length'), true);
assert.deepEqual(res.body, pkg);
});
it('should keep content-length if overwritten with the same stream',
async() => {
const app = new Koa();
app.use(ctx => {
ctx.length = fs.readFileSync('package.json').length;
const stream = fs.createReadStream('package.json');
ctx.body = stream;
ctx.body = stream;
ctx.set('Content-Type', 'application/json; charset=utf-8');
});
const server = app.listen();
const res = await request(server)
.get('/')
.expect('Content-Type', 'application/json; charset=utf-8');
const pkg = require('../../package');
assert.equal(res.headers.hasOwnProperty('content-length'), true);
assert.deepEqual(res.body, pkg);
});
it('should handle errors', done => {
const app = new Koa();
app.use(ctx => {
ctx.set('Content-Type', 'application/json; charset=utf-8');
ctx.body = fs.createReadStream('does not exist');
});
const server = app.listen();
request(server)
.get('/')
.expect('Content-Type', 'text/plain; charset=utf-8')
.expect(404)
.end(done);
});
it('should handle errors when no content status', () => {
const app = new Koa();
app.use(ctx => {
ctx.status = 204;
ctx.body = fs.createReadStream('does not exist');
});
const server = app.listen();
return request(server)
.get('/')
.expect(204);
});
it('should handle all intermediate stream body errors', done => {
const app = new Koa();
app.use(ctx => {
ctx.body = fs.createReadStream('does not exist');
ctx.body = fs.createReadStream('does not exist');
ctx.body = fs.createReadStream('does not exist');
});
const server = app.listen();
request(server)
.get('/')
.expect(404)
.end(done);
});
});
describe('when .body is an Object', () => {
it('should respond with json', () => {
const app = new Koa();
app.use(ctx => {
ctx.body = { hello: 'world' };
});
const server = app.listen();
return request(server)
.get('/')
.expect('Content-Type', 'application/json; charset=utf-8')
.expect('{"hello":"world"}');
});
});
describe('when an error occurs', () => {
it('should emit "error" on the app', done => {
const app = new Koa();
app.use(ctx => {
throw new Error('boom');
});
app.on('error', err => {
assert.equal(err.message, 'boom');
done();
});
request(app.callback())
.get('/')
.end(() => {});
});
describe('with an .expose property', () => {
it('should expose the message', () => {
const app = new Koa();
app.use(ctx => {
const err = new Error('sorry!');
err.status = 403;
err.expose = true;
throw err;
});
return request(app.callback())
.get('/')
.expect(403, 'sorry!');
});
});
describe('with a .status property', () => {
it('should respond with .status', () => {
const app = new Koa();
app.use(ctx => {
const err = new Error('s3 explodes');
err.status = 403;
throw err;
});
return request(app.callback())
.get('/')
.expect(403, 'Forbidden');
});
});
it('should respond with 500', () => {
const app = new Koa();
app.use(ctx => {
throw new Error('boom!');
});
const server = app.listen();
return request(server)
.get('/')
.expect(500, 'Internal Server Error');
});
it('should be catchable', () => {
const app = new Koa();
app.use((ctx, next) => {
return next().then(() => {
ctx.body = 'Hello';
}).catch(() => {
ctx.body = 'Got error';
});
});
app.use((ctx, next) => {
throw new Error('boom!');
});
const server = app.listen();
return request(server)
.get('/')
.expect(200, 'Got error');
});
});
describe('when status and body property', () => {
it('should 200', () => {
const app = new Koa();
app.use(ctx => {
ctx.status = 304;
ctx.body = 'hello';
ctx.status = 200;
});
const server = app.listen();
return request(server)
.get('/')
.expect(200)
.expect('hello');
});
it('should 204', async() => {
const app = new Koa();
app.use(ctx => {
ctx.status = 200;
ctx.body = 'hello';
ctx.set('content-type', 'text/plain; charset=utf8');
ctx.status = 204;
});
const server = app.listen();
const res = await request(server)
.get('/')
.expect(204);
assert.equal(res.headers.hasOwnProperty('content-type'), false);
});
});
});

View File

@ -0,0 +1,46 @@
'use strict';
const request = require('supertest');
const assert = require('assert');
const Koa = require('../..');
describe('app.response', () => {
const app1 = new Koa();
app1.response.msg = 'hello';
const app2 = new Koa();
const app3 = new Koa();
it('should merge properties', () => {
app1.use((ctx, next) => {
assert.equal(ctx.response.msg, 'hello');
ctx.status = 204;
});
return request(app1.listen())
.get('/')
.expect(204);
});
it('should not affect the original prototype', () => {
app2.use((ctx, next) => {
assert.equal(ctx.response.msg, undefined);
ctx.status = 204;
});
return request(app2.listen())
.get('/')
.expect(204);
});
it('should not include status message in body for http2', async() => {
app3.use((ctx, next) => {
ctx.req.httpVersionMajor = 2;
ctx.status = 404;
});
const response = await request(app3.listen())
.get('/')
.expect(404);
assert.equal(response.text, '404');
});
});

View File

@ -0,0 +1,18 @@
'use strict';
const assert = require('assert');
const Koa = require('../..');
describe('app.toJSON()', () => {
it('should work', () => {
const app = new Koa();
const obj = app.toJSON();
assert.deepEqual({
subdomainOffset: 2,
proxy: false,
env: 'test'
}, obj);
});
});

61
test/application/use.js Normal file
View File

@ -0,0 +1,61 @@
'use strict';
const request = require('supertest');
const assert = require('assert');
const Koa = require('../..');
describe('app.use(fn)', () => {
it('should compose middleware', async() => {
const app = new Koa();
const calls = [];
app.use((ctx, next) => {
calls.push(1);
return next().then(() => {
calls.push(6);
});
});
app.use((ctx, next) => {
calls.push(2);
return next().then(() => {
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(404, 'Not Found'));
return request(app.callback())
.get('/')
.expect(404);
});
it('should throw error for non function', () => {
const app = new Koa();
[null, undefined, 0, false, 'not a function'].forEach(v => {
assert.throws(() => app.use(v), /middleware must be a function!/);
});
});
});

View File

@ -1,21 +0,0 @@
var Stream = require('stream');
var koa = require('..');
exports = module.exports = function(req, res){
var socket = new Stream.Duplex();
req = req || { headers: {}, socket: socket, __proto__: Stream.Readable.prototype };
res = res || { _headers: {}, socket: socket, __proto__: Stream.Writable.prototype };
res.getHeader = function(k){ return res._headers[k.toLowerCase()] };
res.setHeader = function(k, v){ res._headers[k.toLowerCase()] = v };
res.removeHeader = function(k, v){ delete res._headers[k.toLowerCase()] };
return koa().createContext(req, res);
}
exports.request = function(req, res){
return exports(req, res).request;
}
exports.response = function(req, res){
return exports(req, res).response;
}

View File

@ -1,17 +1,19 @@
var context = require('../context');
var assert = require('assert');
'use strict';
describe('ctx.assert(value, status)', function(){
it('should throw an error', function(){
var ctx = context();
const context = require('../helpers/context');
const assert = require('assert');
describe('ctx.assert(value, status)', () => {
it('should throw an error', () => {
const ctx = context();
try {
ctx.assert(false, 404);
throw new Error('asdf');
} catch (err) {
assert(404 == err.status);
assert(err.expose);
assert.equal(err.status, 404);
assert.strictEqual(err.expose, true);
}
})
})
});
});

View File

@ -1,81 +0,0 @@
var request = require('supertest');
var koa = require('../..');
describe('ctx.cookies.set()', function(){
it('should set an unsigned cookie', function(done){
var app = koa();
app.use(function *(next){
this.cookies.set('name', 'jon');
this.status = 204;
})
var server = app.listen();
request(server)
.get('/')
.expect(204)
.end(function(err, res){
if (err) return done(err);
res.headers['set-cookie'].some(function(cookie){
return /^name=/.test(cookie);
}).should.be.ok;
done();
})
})
describe('with .signed', function(){
describe('when no .keys are set', function(){
it('should error', function(done){
var app = koa();
app.use(function *(next){
try {
this.cookies.set('foo', 'bar', { signed: true });
} catch (err) {
this.body = err.message;
}
});
request(app.listen())
.get('/')
.expect('.keys required for signed cookies', done);
})
})
it('should send a signed cookie', function(done){
var app = koa();
app.keys = ['a', 'b'];
app.use(function *(next){
this.cookies.set('name', 'jon', { signed: true });
this.status = 204;
})
var server = app.listen();
request(server)
.get('/')
.expect(204)
.end(function(err, res){
if (err) return done(err);
var cookies = res.headers['set-cookie'];
cookies.some(function(cookie){
return /^name=/.test(cookie);
}).should.be.ok;
cookies.some(function(cookie){
return /^name\.sig=/.test(cookie);
}).should.be.ok;
done();
})
})
})
})

View File

@ -1,11 +1,23 @@
var context = require('../context');
'use strict';
describe('ctx.inspect()', function(){
it('should return a json representation', function(){
var ctx = context();
var toJSON = ctx.toJSON(ctx);
const prototype = require('../../lib/context');
const assert = require('assert');
const util = require('util');
const context = require('../helpers/context');
toJSON.should.eql(ctx.inspect());
})
})
describe('ctx.inspect()', () => {
it('should return a json representation', () => {
const ctx = context();
const toJSON = ctx.toJSON(ctx);
assert.deepEqual(toJSON, ctx.inspect());
assert.deepEqual(util.inspect(toJSON), util.inspect(ctx));
});
// console.log(require.cache) will call prototype.inspect()
it('should not crash when called on the prototype', () => {
assert.deepEqual(prototype, prototype.inspect());
assert.deepEqual(util.inspect(prototype.inspect()), util.inspect(prototype));
});
});

View File

@ -1,114 +1,218 @@
var request = require('supertest');
var koa = require('../..');
'use strict';
describe('ctx.onerror(err)', function(){
it('should respond', function(done){
var app = koa();
const assert = require('assert');
const request = require('supertest');
const Koa = require('../..');
const context = require('../helpers/context');
app.use(function *(next){
this.body = 'something else';
describe('ctx.onerror(err)', () => {
it('should respond', () => {
const app = new Koa();
this.throw(418, 'boom');
})
app.use((ctx, next) => {
ctx.body = 'something else';
var server = app.listen();
ctx.throw(418, 'boom');
});
request(server)
.get('/')
.expect(418)
.expect('Content-Type', 'text/plain; charset=utf-8')
.expect('Content-Length', '4')
.end(done);
})
const server = app.listen();
it('should unset all headers', function(done){
var app = koa();
return request(server)
.get('/')
.expect(418)
.expect('Content-Type', 'text/plain; charset=utf-8')
.expect('Content-Length', '4');
});
app.use(function *(next){
this.set('Vary', 'Accept-Encoding');
this.set('X-CSRF-Token', 'asdf');
this.body = 'response';
it('should unset all headers', async() => {
const app = new Koa();
this.throw(418, 'boom');
})
app.use((ctx, next) => {
ctx.set('Vary', 'Accept-Encoding');
ctx.set('X-CSRF-Token', 'asdf');
ctx.body = 'response';
var server = app.listen();
ctx.throw(418, 'boom');
});
request(server)
.get('/')
.expect(418)
.expect('Content-Type', 'text/plain; charset=utf-8')
.expect('Content-Length', '4')
.end(function(err, res){
if (err) return done(err);
const server = app.listen();
res.headers.should.not.have.property('vary');
res.headers.should.not.have.property('x-csrf-token');
const res = await request(server)
.get('/')
.expect(418)
.expect('Content-Type', 'text/plain; charset=utf-8')
.expect('Content-Length', '4');
assert.equal(res.headers.hasOwnProperty('vary'), false);
assert.equal(res.headers.hasOwnProperty('x-csrf-token'), false);
});
it('should set headers specified in the error', async() => {
const app = new Koa();
app.use((ctx, next) => {
ctx.set('Vary', 'Accept-Encoding');
ctx.set('X-CSRF-Token', 'asdf');
ctx.body = 'response';
throw Object.assign(new Error('boom'), {
status: 418,
expose: true,
headers: {
'X-New-Header': 'Value'
}
});
});
const server = app.listen();
const res = await request(server)
.get('/')
.expect(418)
.expect('Content-Type', 'text/plain; charset=utf-8')
.expect('X-New-Header', 'Value');
assert.equal(res.headers.hasOwnProperty('vary'), false);
assert.equal(res.headers.hasOwnProperty('x-csrf-token'), false);
});
it('should ignore error after headerSent', done => {
const app = new Koa();
app.on('error', err => {
assert.equal(err.message, 'mock error');
assert.equal(err.headerSent, true);
done();
})
})
});
describe('when invalid err.status', function(){
describe('not number', function(){
it('should respond 500', function(done){
var app = koa();
app.use(async ctx => {
ctx.status = 200;
ctx.set('X-Foo', 'Bar');
ctx.flushHeaders();
await Promise.reject(new Error('mock error'));
ctx.body = 'response';
});
app.use(function *(next){
this.body = 'something else';
var err = new Error('some error');
request(app.callback())
.get('/')
.expect('X-Foo', 'Bar')
.expect(200, () => {});
});
describe('when invalid err.status', () => {
describe('not number', () => {
it('should respond 500', () => {
const app = new Koa();
app.use((ctx, next) => {
ctx.body = 'something else';
const err = new Error('some error');
err.status = 'notnumber';
throw err;
})
});
var server = app.listen();
const server = app.listen();
request(server)
.get('/')
.expect(500)
.expect('Content-Type', 'text/plain; charset=utf-8')
.expect('Internal Server Error', done);
})
})
return request(server)
.get('/')
.expect(500)
.expect('Content-Type', 'text/plain; charset=utf-8')
.expect('Internal Server Error');
});
});
describe('when ENOENT error', () => {
it('should respond 404', () => {
const app = new Koa();
describe('not http status code', function(){
it('should respond 500', function(done){
var app = koa();
app.use((ctx, next) => {
ctx.body = 'something else';
const err = new Error('test for ENOENT');
err.code = 'ENOENT';
throw err;
});
app.use(function *(next){
this.body = 'something else';
var err = new Error('some error');
const server = app.listen();
return request(server)
.get('/')
.expect(404)
.expect('Content-Type', 'text/plain; charset=utf-8')
.expect('Not Found');
});
});
describe('not http status code', () => {
it('should respond 500', () => {
const app = new Koa();
app.use((ctx, next) => {
ctx.body = 'something else';
const err = new Error('some error');
err.status = 9999;
throw err;
})
});
var server = app.listen();
const server = app.listen();
request(server)
return request(server)
.get('/')
.expect(500)
.expect('Content-Type', 'text/plain; charset=utf-8')
.expect('Internal Server Error');
});
});
});
describe('when non-error thrown', () => {
it('should response non-error thrown message', () => {
const app = new Koa();
app.use((ctx, next) => {
throw 'string error'; // eslint-disable-line no-throw-literal
});
const server = app.listen();
return request(server)
.get('/')
.expect(500)
.expect('Content-Type', 'text/plain; charset=utf-8')
.expect('Internal Server Error', done);
})
})
})
.expect('Internal Server Error');
});
describe('when non-error thrown', function(){
it('should response non-error thrown message', function(done){
var app = koa();
it('should use res.getHeaderNames() accessor when available', () => {
let removed = 0;
const ctx = context();
app.use(function *(next){
throw 'string error';
})
ctx.app.emit = () => {};
ctx.res = {
getHeaderNames: () => ['content-type', 'content-length'],
removeHeader: () => removed++,
end: () => {},
emit: () => {}
};
var server = app.listen();
ctx.onerror(new Error('error'));
request(server)
.get('/')
.expect(500)
.expect('Content-Type', 'text/plain; charset=utf-8')
.expect('Internal Server Error', done);
})
})
})
assert.equal(removed, 2);
});
it('should stringify error if it is an object', done => {
const app = new Koa();
app.on('error', err => {
assert.equal(err, 'Error: non-error thrown: {"key":"value"}');
done();
});
app.use(async ctx => {
throw {key: 'value'}; // eslint-disable-line no-throw-literal
});
request(app.callback())
.get('/')
.expect(500)
.expect('Internal Server Error', () => {});
});
});
});

View File

@ -1,21 +1,22 @@
var request = require('supertest');
var assert = require('assert');
var koa = require('../..');
'use strict';
describe('ctx.state', function() {
it('should provide a ctx.state namespace', function(done) {
var app = koa();
const request = require('supertest');
const assert = require('assert');
const Koa = require('../..');
app.use(function *() {
assert.deepEqual(this.state, {});
describe('ctx.state', () => {
it('should provide a ctx.state namespace', () => {
const app = new Koa();
app.use(ctx => {
assert.deepEqual(ctx.state, {});
});
var server = app.listen();
const server = app.listen();
request(server)
.get('/')
.expect(404)
.end(done);
})
})
return request(server)
.get('/')
.expect(404);
});
});

View File

@ -1,148 +1,112 @@
var context = require('../context');
var assert = require('assert');
'use strict';
describe('ctx.throw(msg)', function(){
it('should set .status to 500', function(done){
var ctx = context();
const context = require('../helpers/context');
const assert = require('assert');
describe('ctx.throw(msg)', () => {
it('should set .status to 500', () => {
const ctx = context();
try {
ctx.throw('boom');
} catch (err) {
assert(500 == err.status);
assert(!err.expose);
done();
assert.equal(err.status, 500);
assert.equal(err.expose, false);
}
})
})
});
});
describe('ctx.throw(err)', function(){
it('should set .status to 500', function(done){
var ctx = context();
var err = new Error('test');
describe('ctx.throw(err)', () => {
it('should set .status to 500', () => {
const ctx = context();
const err = new Error('test');
try {
ctx.throw(err);
} catch (err) {
assert(500 == err.status);
assert('test' == err.message);
assert(!err.expose);
done();
assert.equal(err.status, 500);
assert.equal(err.message, 'test');
assert.equal(err.expose, false);
}
})
})
});
});
describe('ctx.throw(err, status)', function(){
it('should throw the error and set .status', function(done){
var ctx = context();
var error = new Error('test');
try {
ctx.throw(error, 422);
} catch (err) {
assert(422 == err.status);
assert('test' == err.message);
assert(true === err.expose);
done();
}
})
})
describe('ctx.throw(status, err)', function(){
it('should throw the error and set .status', function(done){
var ctx = context();
var error = new Error('test');
describe('ctx.throw(status, err)', () => {
it('should throw the error and set .status', () => {
const ctx = context();
const error = new Error('test');
try {
ctx.throw(422, error);
} catch (err) {
assert(422 == err.status);
assert('test' == err.message);
assert(true === err.expose);
done();
assert.equal(err.status, 422);
assert.equal(err.message, 'test');
assert.equal(err.expose, true);
}
})
})
});
});
describe('ctx.throw(msg, status)', function(){
it('should throw an error', function(done){
var ctx = context();
try {
ctx.throw('name required', 400);
} catch (err) {
assert('name required' == err.message);
assert(400 == err.status);
assert(true === err.expose);
done();
}
})
})
describe('ctx.throw(status, msg)', function(){
it('should throw an error', function(done){
var ctx = context();
describe('ctx.throw(status, msg)', () => {
it('should throw an error', () => {
const ctx = context();
try {
ctx.throw(400, 'name required');
} catch (err) {
assert('name required' == err.message);
assert(400 == err.status);
assert(true === err.expose);
done();
assert.equal(err.message, 'name required');
assert.equal(400, err.status);
assert.equal(true, err.expose);
}
})
})
});
});
describe('ctx.throw(status)', function(){
it('should throw an error', function(done){
var ctx = context();
describe('ctx.throw(status)', () => {
it('should throw an error', () => {
const ctx = context();
try {
ctx.throw(400);
} catch (err) {
assert('Bad Request' == err.message);
assert(400 == err.status);
assert(true === err.expose);
done();
assert.equal(err.message, 'Bad Request');
assert.equal(err.status, 400);
assert.equal(err.expose, true);
}
})
});
describe('when not valid status', function(){
it('should not expose', function(done){
var ctx = context();
describe('when not valid status', () => {
it('should not expose', () => {
const ctx = context();
try {
var err = new Error('some error');
const err = new Error('some error');
err.status = -1;
ctx.throw(err);
} catch(err) {
assert('some error' == err.message);
assert(!err.expose);
done();
} catch (err) {
assert.equal(err.message, 'some error');
assert.equal(err.expose, false);
}
})
})
})
});
});
});
describe('ctx.throw(status, msg, props)', function(){
it('should mixin props', function(done){
var ctx = context();
describe('ctx.throw(status, msg, props)', () => {
it('should mixin props', () => {
const ctx = context();
try {
ctx.throw(400, 'msg', { prop: true });
} catch (err) {
assert('msg' == err.message);
assert(400 == err.status);
assert(true === err.expose);
assert(true === err.prop);
done();
assert.equal(err.message, 'msg');
assert.equal(err.status, 400);
assert.equal(err.expose, true);
assert.equal(err.prop, true);
}
})
});
describe('when props include status', function(){
it('should be ignored', function(done){
var ctx = context();
describe('when props include status', () => {
it('should be ignored', () => {
const ctx = context();
try {
ctx.throw(400, 'msg', {
@ -150,60 +114,56 @@ describe('ctx.throw(status, msg, props)', function(){
status: -1
});
} catch (err) {
assert('msg' == err.message);
assert(400 == err.status);
assert(true === err.expose);
assert(true === err.prop);
done();
assert.equal(err.message, 'msg');
assert.equal(err.status, 400);
assert.equal(err.expose, true);
assert.equal(err.prop, true);
}
})
})
})
});
});
});
describe('ctx.throw(msg, props)', function(){
it('should mixin props', function(done){
var ctx = context();
describe('ctx.throw(msg, props)', () => {
it('should mixin props', () => {
const ctx = context();
try {
ctx.throw('msg', { prop: true });
} catch (err) {
assert('msg' == err.message);
assert(500 == err.status);
assert(false === err.expose);
assert(true === err.prop);
done();
assert.equal(err.message, 'msg');
assert.equal(err.status, 500);
assert.equal(err.expose, false);
assert.equal(err.prop, true);
}
})
})
});
});
describe('ctx.throw(status, props)', function(){
it('should mixin props', function(done){
var ctx = context();
describe('ctx.throw(status, props)', () => {
it('should mixin props', () => {
const ctx = context();
try {
ctx.throw(400, { prop: true });
} catch (err) {
assert('Bad Request' == err.message);
assert(400 == err.status);
assert(true === err.expose);
assert(true === err.prop);
done();
assert.equal(err.message, 'Bad Request');
assert.equal(err.status, 400);
assert.equal(err.expose, true);
assert.equal(err.prop, true);
}
})
})
});
});
describe('ctx.throw(err, props)', function(){
it('should mixin props', function(done){
var ctx = context();
describe('ctx.throw(err, props)', () => {
it('should mixin props', () => {
const ctx = context();
try {
ctx.throw(new Error('test'), { prop: true });
} catch (err) {
assert('test' == err.message);
assert(500 == err.status);
assert(false === err.expose);
assert(true === err.prop);
done();
assert.equal(err.message, 'test');
assert.equal(err.status, 500);
assert.equal(err.expose, false);
assert.equal(err.prop, true);
}
})
})
});
});

View File

@ -1,9 +1,12 @@
var context = require('../context');
'use strict';
describe('ctx.toJSON()', function(){
it('should return a json representation', function(){
var ctx = context();
const assert = require('assert');
const context = require('../helpers/context');
describe('ctx.toJSON()', () => {
it('should return a json representation', () => {
const ctx = context();
ctx.req.method = 'POST';
ctx.req.url = '/items';
@ -11,25 +14,25 @@ describe('ctx.toJSON()', function(){
ctx.status = 200;
ctx.body = '<p>Hey</p>';
var obj = JSON.parse(JSON.stringify(ctx));
var req = obj.request;
var res = obj.response;
const obj = JSON.parse(JSON.stringify(ctx));
const req = obj.request;
const res = obj.response;
req.should.eql({
assert.deepEqual({
method: 'POST',
url: '/items',
header: {
'content-type': 'text/plain'
}
});
}, req);
res.should.eql({
assert.deepEqual({
status: 200,
message: 'OK',
header: {
'content-type': 'text/html; charset=utf-8',
'content-length': '10'
}
});
})
})
}, res);
});
});

View File

@ -1,23 +0,0 @@
/**
* Separate file primarily because we use `require('babel/register')`.
*/
var request = require('supertest');
var koa = require('../..');
describe('.experimental=true', function () {
it('should support async functions', function (done) {
var app = koa();
app.experimental = true;
app.use(async function (next) {
var string = await Promise.resolve('asdf');
this.body = string;
});
request(app.callback())
.get('/')
.expect('asdf')
.expect(200, done);
})
})

View File

@ -1,4 +0,0 @@
require('babel/register')({
optional: ['asyncToGenerator']
});
require('./async');

21
test/helpers/context.js Normal file
View File

@ -0,0 +1,21 @@
'use strict';
const Stream = require('stream');
const Koa = require('../..');
module.exports = (req, res, app) => {
const socket = new Stream.Duplex();
req = Object.assign({ headers: {}, socket }, Stream.Readable.prototype, req);
res = Object.assign({ _headers: {}, socket }, Stream.Writable.prototype, res);
req.socket.remoteAddress = req.socket.remoteAddress || '127.0.0.1';
app = app || new Koa();
res.getHeader = k => res._headers[k.toLowerCase()];
res.setHeader = (k, v) => res._headers[k.toLowerCase()] = v;
res.removeHeader = (k, v) => delete res._headers[k.toLowerCase()];
return app.createContext(req, res);
};
module.exports.request = (req, res, app) => module.exports(req, res, app).request;
module.exports.response = (req, res, app) => module.exports(req, res, app).response;

View File

@ -1,90 +1,94 @@
var context = require('../context');
'use strict';
describe('ctx.accepts(types)', function(){
describe('with no arguments', function(){
describe('when Accept is populated', function(){
it('should return all accepted types', function(){
var ctx = context();
const assert = require('assert');
const context = require('../helpers/context');
describe('ctx.accepts(types)', () => {
describe('with no arguments', () => {
describe('when Accept is populated', () => {
it('should return all accepted types', () => {
const ctx = context();
ctx.req.headers.accept = 'application/*;q=0.2, image/jpeg;q=0.8, text/html, text/plain';
ctx.accepts().should.eql(['text/html', 'text/plain', 'image/jpeg', 'application/*']);
})
})
})
assert.deepEqual(ctx.accepts(), ['text/html', 'text/plain', 'image/jpeg', 'application/*']);
});
});
});
describe('with no valid types', function(){
describe('when Accept is populated', function(){
it('should return false', function(){
var ctx = context();
describe('with no valid types', () => {
describe('when Accept is populated', () => {
it('should return false', () => {
const ctx = context();
ctx.req.headers.accept = 'application/*;q=0.2, image/jpeg;q=0.8, text/html, text/plain';
ctx.accepts('image/png', 'image/tiff').should.be.false;
})
})
assert.equal(ctx.accepts('image/png', 'image/tiff'), false);
});
});
describe('when Accept is not populated', function(){
it('should return the first type', function(){
var ctx = context();
ctx.accepts('text/html', 'text/plain', 'image/jpeg', 'application/*').should.equal('text/html');
})
})
})
describe('when Accept is not populated', () => {
it('should return the first type', () => {
const ctx = context();
assert.equal(ctx.accepts('text/html', 'text/plain', 'image/jpeg', 'application/*'), 'text/html');
});
});
});
describe('when extensions are given', function(){
it('should convert to mime types', function(){
var ctx = context();
describe('when extensions are given', () => {
it('should convert to mime types', () => {
const ctx = context();
ctx.req.headers.accept = 'text/plain, text/html';
ctx.accepts('html').should.equal('html');
ctx.accepts('.html').should.equal('.html');
ctx.accepts('txt').should.equal('txt');
ctx.accepts('.txt').should.equal('.txt');
ctx.accepts('png').should.be.false;
})
})
assert.equal(ctx.accepts('html'), 'html');
assert.equal(ctx.accepts('.html'), '.html');
assert.equal(ctx.accepts('txt'), 'txt');
assert.equal(ctx.accepts('.txt'), '.txt');
assert.equal(ctx.accepts('png'), false);
});
});
describe('when an array is given', function(){
it('should return the first match', function(){
var ctx = context();
describe('when an array is given', () => {
it('should return the first match', () => {
const ctx = context();
ctx.req.headers.accept = 'text/plain, text/html';
ctx.accepts(['png', 'text', 'html']).should.equal('text');
ctx.accepts(['png', 'html']).should.equal('html');
})
})
assert.equal(ctx.accepts(['png', 'text', 'html']), 'text');
assert.equal(ctx.accepts(['png', 'html']), 'html');
});
});
describe('when multiple arguments are given', function(){
it('should return the first match', function(){
var ctx = context();
describe('when multiple arguments are given', () => {
it('should return the first match', () => {
const ctx = context();
ctx.req.headers.accept = 'text/plain, text/html';
ctx.accepts('png', 'text', 'html').should.equal('text');
ctx.accepts('png', 'html').should.equal('html');
})
})
assert.equal(ctx.accepts('png', 'text', 'html'), 'text');
assert.equal(ctx.accepts('png', 'html'), 'html');
});
});
describe('when present in Accept as an exact match', function(){
it('should return the type', function(){
var ctx = context();
describe('when present in Accept as an exact match', () => {
it('should return the type', () => {
const ctx = context();
ctx.req.headers.accept = 'text/plain, text/html';
ctx.accepts('text/html').should.equal('text/html');
ctx.accepts('text/plain').should.equal('text/plain');
})
})
assert.equal(ctx.accepts('text/html'), 'text/html');
assert.equal(ctx.accepts('text/plain'), 'text/plain');
});
});
describe('when present in Accept as a type match', function(){
it('should return the type', function(){
var ctx = context();
describe('when present in Accept as a type match', () => {
it('should return the type', () => {
const ctx = context();
ctx.req.headers.accept = 'application/json, */*';
ctx.accepts('text/html').should.equal('text/html');
ctx.accepts('text/plain').should.equal('text/plain');
ctx.accepts('image/png').should.equal('image/png');
})
})
assert.equal(ctx.accepts('text/html'), 'text/html');
assert.equal(ctx.accepts('text/plain'), 'text/plain');
assert.equal(ctx.accepts('image/png'), 'image/png');
});
});
describe('when present in Accept as a subtype match', function(){
it('should return the type', function(){
var ctx = context();
describe('when present in Accept as a subtype match', () => {
it('should return the type', () => {
const ctx = context();
ctx.req.headers.accept = 'application/json, text/*';
ctx.accepts('text/html').should.equal('text/html');
ctx.accepts('text/plain').should.equal('text/plain');
ctx.accepts('image/png').should.be.false;
})
})
})
assert.equal(ctx.accepts('text/html'), 'text/html');
assert.equal(ctx.accepts('text/plain'), 'text/plain');
assert.equal(ctx.accepts('image/png'), false);
assert.equal(ctx.accepts('png'), false);
});
});
});

View File

@ -1,49 +1,52 @@
var context = require('../context');
'use strict';
describe('ctx.acceptsCharsets()', function(){
describe('with no arguments', function(){
describe('when Accept-Charset is populated', function(){
it('should return accepted types', function(){
var ctx = context();
const assert = require('assert');
const context = require('../helpers/context');
describe('ctx.acceptsCharsets()', () => {
describe('with no arguments', () => {
describe('when Accept-Charset is populated', () => {
it('should return accepted types', () => {
const ctx = context();
ctx.req.headers['accept-charset'] = 'utf-8, iso-8859-1;q=0.2, utf-7;q=0.5';
ctx.acceptsCharsets().should.eql(['utf-8', 'utf-7', 'iso-8859-1']);
})
})
})
assert.deepEqual(ctx.acceptsCharsets(), ['utf-8', 'utf-7', 'iso-8859-1']);
});
});
});
describe('with multiple arguments', function(){
describe('when Accept-Charset is populated', function(){
describe('if any types match', function(){
it('should return the best fit', function(){
var ctx = context();
describe('with multiple arguments', () => {
describe('when Accept-Charset is populated', () => {
describe('if any types match', () => {
it('should return the best fit', () => {
const ctx = context();
ctx.req.headers['accept-charset'] = 'utf-8, iso-8859-1;q=0.2, utf-7;q=0.5';
ctx.acceptsCharsets('utf-7', 'utf-8').should.equal('utf-8');
})
})
assert.equal(ctx.acceptsCharsets('utf-7', 'utf-8'), 'utf-8');
});
});
describe('if no types match', function(){
it('should return false', function(){
var ctx = context();
describe('if no types match', () => {
it('should return false', () => {
const ctx = context();
ctx.req.headers['accept-charset'] = 'utf-8, iso-8859-1;q=0.2, utf-7;q=0.5';
ctx.acceptsCharsets('utf-16').should.be.false;
})
})
})
assert.equal(ctx.acceptsCharsets('utf-16'), false);
});
});
});
describe('when Accept-Charset is not populated', function(){
it('should return the first type', function(){
var ctx = context();
ctx.acceptsCharsets('utf-7', 'utf-8').should.equal('utf-7');
})
})
})
describe('when Accept-Charset is not populated', () => {
it('should return the first type', () => {
const ctx = context();
assert.equal(ctx.acceptsCharsets('utf-7', 'utf-8'), 'utf-7');
});
});
});
describe('with an array', function(){
it('should return the best fit', function(){
var ctx = context();
describe('with an array', () => {
it('should return the best fit', () => {
const ctx = context();
ctx.req.headers['accept-charset'] = 'utf-8, iso-8859-1;q=0.2, utf-7;q=0.5';
ctx.acceptsCharsets(['utf-7', 'utf-8']).should.equal('utf-8');
})
})
})
assert.equal(ctx.acceptsCharsets(['utf-7', 'utf-8']), 'utf-8');
});
});
});

View File

@ -1,40 +1,43 @@
var context = require('../context');
'use strict';
describe('ctx.acceptsEncodings()', function(){
describe('with no arguments', function(){
describe('when Accept-Encoding is populated', function(){
it('should return accepted types', function(){
var ctx = context();
const assert = require('assert');
const context = require('../helpers/context');
describe('ctx.acceptsEncodings()', () => {
describe('with no arguments', () => {
describe('when Accept-Encoding is populated', () => {
it('should return accepted types', () => {
const ctx = context();
ctx.req.headers['accept-encoding'] = 'gzip, compress;q=0.2';
ctx.acceptsEncodings().should.eql(['gzip', 'compress', 'identity']);
ctx.acceptsEncodings('gzip', 'compress').should.equal('gzip');
})
})
assert.deepEqual(ctx.acceptsEncodings(), ['gzip', 'compress', 'identity']);
assert.equal(ctx.acceptsEncodings('gzip', 'compress'), 'gzip');
});
});
describe('when Accept-Encoding is not populated', function(){
it('should return identity', function(){
var ctx = context();
ctx.acceptsEncodings().should.eql(['identity']);
ctx.acceptsEncodings('gzip', 'deflate', 'identity').should.equal('identity');
})
})
})
describe('when Accept-Encoding is not populated', () => {
it('should return identity', () => {
const ctx = context();
assert.deepEqual(ctx.acceptsEncodings(), ['identity']);
assert.equal(ctx.acceptsEncodings('gzip', 'deflate', 'identity'), 'identity');
});
});
});
describe('with multiple arguments', function(){
it('should return the best fit', function(){
var ctx = context();
describe('with multiple arguments', () => {
it('should return the best fit', () => {
const ctx = context();
ctx.req.headers['accept-encoding'] = 'gzip, compress;q=0.2';
ctx.acceptsEncodings('compress', 'gzip').should.eql('gzip');
ctx.acceptsEncodings('gzip', 'compress').should.eql('gzip');
})
})
assert.equal(ctx.acceptsEncodings('compress', 'gzip'), 'gzip');
assert.equal(ctx.acceptsEncodings('gzip', 'compress'), 'gzip');
});
});
describe('with an array', function(){
it('should return the best fit', function(){
var ctx = context();
describe('with an array', () => {
it('should return the best fit', () => {
const ctx = context();
ctx.req.headers['accept-encoding'] = 'gzip, compress;q=0.2';
ctx.acceptsEncodings(['compress', 'gzip']).should.eql('gzip');
})
})
})
assert.equal(ctx.acceptsEncodings(['compress', 'gzip']), 'gzip');
});
});
});

View File

@ -1,49 +1,52 @@
var context = require('../context');
'use strict';
describe('ctx.acceptsLanguages(langs)', function(){
describe('with no arguments', function(){
describe('when Accept-Language is populated', function(){
it('should return accepted types', function(){
var ctx = context();
const assert = require('assert');
const context = require('../helpers/context');
describe('ctx.acceptsLanguages(langs)', () => {
describe('with no arguments', () => {
describe('when Accept-Language is populated', () => {
it('should return accepted types', () => {
const ctx = context();
ctx.req.headers['accept-language'] = 'en;q=0.8, es, pt';
ctx.acceptsLanguages().should.eql(['es', 'pt', 'en']);
})
})
})
assert.deepEqual(ctx.acceptsLanguages(), ['es', 'pt', 'en']);
});
});
});
describe('with multiple arguments', function(){
describe('when Accept-Language is populated', function(){
describe('if any types types match', function(){
it('should return the best fit', function(){
var ctx = context();
describe('with multiple arguments', () => {
describe('when Accept-Language is populated', () => {
describe('if any types types match', () => {
it('should return the best fit', () => {
const ctx = context();
ctx.req.headers['accept-language'] = 'en;q=0.8, es, pt';
ctx.acceptsLanguages('es', 'en').should.equal('es');
})
})
assert.equal(ctx.acceptsLanguages('es', 'en'), 'es');
});
});
describe('if no types match', function(){
it('should return false', function(){
var ctx = context();
describe('if no types match', () => {
it('should return false', () => {
const ctx = context();
ctx.req.headers['accept-language'] = 'en;q=0.8, es, pt';
ctx.acceptsLanguages('fr', 'au').should.be.false;
})
})
})
assert.equal(ctx.acceptsLanguages('fr', 'au'), false);
});
});
});
describe('when Accept-Language is not populated', function(){
it('should return the first type', function(){
var ctx = context();
ctx.acceptsLanguages('es', 'en').should.equal('es');
})
})
})
describe('when Accept-Language is not populated', () => {
it('should return the first type', () => {
const ctx = context();
assert.equal(ctx.acceptsLanguages('es', 'en'), 'es');
});
});
});
describe('with an array', function(){
it('should return the best fit', function(){
var ctx = context();
describe('with an array', () => {
it('should return the best fit', () => {
const ctx = context();
ctx.req.headers['accept-language'] = 'en;q=0.8, es, pt';
ctx.acceptsLanguages(['es', 'en']).should.equal('es');
})
})
})
assert.equal(ctx.acceptsLanguages(['es', 'en']), 'es');
});
});
});

View File

@ -1,28 +0,0 @@
var request = require('../context').request;
var assert = require('assert');
describe('req.charset', function(){
describe('with no content-type present', function(){
it('should return ""', function(){
var req = request();
assert('' === req.charset);
})
})
describe('with charset present', function(){
it('should return ""', function(){
var req = request();
req.header['content-type'] = 'text/plain';
assert('' === req.charset);
})
})
describe('with a charset', function(){
it('should return the charset', function(){
var req = request();
req.header['content-type'] = 'text/plain; charset=utf-8';
req.charset.should.equal('utf-8');
})
})
})

View File

@ -1,46 +1,236 @@
var context = require('../context');
'use strict';
describe('ctx.fresh', function(){
describe('the request method is not GET and HEAD', function (){
it('should return false', function (){
var ctx = context();
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', () => {
it('should return false', () => {
const ctx = context();
ctx.req.method = 'POST';
ctx.fresh.should.be.false;
})
})
assert.equal(ctx.fresh, false);
});
});
describe('the response is non-2xx', function(){
it('should return false', function(){
var ctx = context();
describe('the response is non-2xx', () => {
it('should return false', () => {
const ctx = context();
ctx.status = 404;
ctx.req.method = 'GET';
ctx.req.headers['if-none-match'] = '123';
ctx.set('ETag', '123');
ctx.fresh.should.be.false;
})
assert.equal(ctx.fresh, false);
});
});
describe('the response is 2xx', function(){
describe('and etag matches', function(){
it('should return true', function(){
var ctx = context();
describe('the response is 2xx', () => {
describe('and etag matches', () => {
it('should return true', () => {
const ctx = context();
ctx.status = 200;
ctx.req.method = 'GET';
ctx.req.headers['if-none-match'] = '123';
ctx.set('ETag', '123');
ctx.fresh.should.be.true;
})
})
assert.equal(ctx.fresh, true);
});
});
describe('and etag do not match', function(){
it('should return false', function(){
var ctx = context();
describe('and etag do not match', () => {
it('should return false', () => {
const ctx = context();
ctx.status = 200;
ctx.req.method = 'GET';
ctx.req.headers['if-none-match'] = '123';
ctx.set('ETag', 'hey');
ctx.fresh.should.be.false;
assert.equal(ctx.fresh, false);
});
});
});
});
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

@ -1,15 +1,18 @@
var context = require('../context');
'use strict';
describe('ctx.get(name)', function(){
it('should return the field value', function(){
var ctx = context();
const assert = require('assert');
const context = require('../helpers/context');
describe('ctx.get(name)', () => {
it('should return the field value', () => {
const ctx = context();
ctx.req.headers.host = 'http://google.com';
ctx.req.headers.referer = 'http://google.com';
ctx.get('HOST').should.equal('http://google.com');
ctx.get('Host').should.equal('http://google.com');
ctx.get('host').should.equal('http://google.com');
ctx.get('referer').should.equal('http://google.com');
ctx.get('referrer').should.equal('http://google.com');
})
})
assert.equal(ctx.get('HOST'), 'http://google.com');
assert.equal(ctx.get('Host'), 'http://google.com');
assert.equal(ctx.get('host'), 'http://google.com');
assert.equal(ctx.get('referer'), 'http://google.com');
assert.equal(ctx.get('referrer'), 'http://google.com');
});
});

View File

@ -1,9 +1,18 @@
var request = require('../context').request;
'use strict';
describe('req.header', function(){
it('should return the request header object', function(){
var req = request();
req.header.should.equal(req.req.headers);
})
})
const assert = require('assert');
const request = require('../helpers/context').request;
describe('req.header', () => {
it('should return the request header object', () => {
const req = request();
assert.deepEqual(req.header, req.req.headers);
});
it('should set the request header object', () => {
const req = request();
req.header = {'X-Custom-Headerfield': 'Its one header, with headerfields'};
assert.deepEqual(req.header, req.req.headers);
});
});

View File

@ -1,9 +1,18 @@
var request = require('../context').request;
'use strict';
describe('req.headers', function(){
it('should return the request header object', function(){
var req = request();
req.headers.should.equal(req.req.headers);
})
})
const assert = require('assert');
const request = require('../helpers/context').request;
describe('req.headers', () => {
it('should return the request header object', () => {
const req = request();
assert.deepEqual(req.headers, req.req.headers);
});
it('should set the request header object', () => {
const req = request();
req.headers = {'X-Custom-Headerfield': 'Its one header, with headerfields'};
assert.deepEqual(req.headers, req.req.headers);
});
});

View File

@ -1,39 +1,97 @@
var request = require('../context').request;
var assert = require('assert');
'use strict';
describe('req.host', function(){
it('should return host with port', function(){
var req = request();
const request = require('../helpers/context').request;
const assert = require('assert');
describe('req.host', () => {
it('should return host with port', () => {
const req = request();
req.header.host = 'foo.com:3000';
req.host.should.equal('foo.com:3000');
})
assert.equal(req.host, 'foo.com:3000');
});
describe('with no host present', function(){
it('should return ""', function(){
var req = request();
describe('with no host present', () => {
it('should return ""', () => {
const req = request();
assert.equal(req.host, '');
})
})
});
});
describe('when X-Forwarded-Host is present', function(){
describe('and proxy is not trusted', function(){
it('should be ignored', function(){
var req = request();
describe('when less then HTTP/2', () => {
it('should not use :authority header', () => {
const req = request({
'httpVersionMajor': 1,
'httpVersion': '1.1'
});
req.header[':authority'] = 'foo.com:3000';
req.header.host = 'bar.com:8000';
assert.equal(req.host, 'bar.com:8000');
});
});
describe('when HTTP/2', () => {
it('should use :authority header', () => {
const req = request({
'httpVersionMajor': 2,
'httpVersion': '2.0'
});
req.header[':authority'] = 'foo.com:3000';
req.header.host = 'bar.com:8000';
assert.equal(req.host, 'foo.com:3000');
});
it('should use host header as fallback', () => {
const req = request({
'httpVersionMajor': 2,
'httpVersion': '2.0'
});
req.header.host = 'bar.com:8000';
assert.equal(req.host, 'bar.com:8000');
});
});
describe('when X-Forwarded-Host is present', () => {
describe('and proxy is not trusted', () => {
it('should be ignored on HTTP/1', () => {
const req = request();
req.header['x-forwarded-host'] = 'bar.com';
req.header['host'] = 'foo.com';
req.host.should.equal('foo.com');
})
})
req.header.host = 'foo.com';
assert.equal(req.host, 'foo.com');
});
describe('and proxy is trusted', function(){
it('should be used', function(){
var req = request();
it('should be ignored on HTTP/2', () => {
const req = request({
'httpVersionMajor': 2,
'httpVersion': '2.0'
});
req.header['x-forwarded-host'] = 'proxy.com:8080';
req.header[':authority'] = 'foo.com:3000';
req.header.host = 'bar.com:8000';
assert.equal(req.host, 'foo.com:3000');
});
});
describe('and proxy is trusted', () => {
it('should be used on HTTP/1', () => {
const req = request();
req.app.proxy = true;
req.header['x-forwarded-host'] = 'bar.com, baz.com';
req.header['host'] = 'foo.com';
req.host.should.equal('bar.com');
})
})
})
})
req.header.host = 'foo.com';
assert.equal(req.host, 'bar.com');
});
it('should be used on HTTP/2', () => {
const req = request({
'httpVersionMajor': 2,
'httpVersion': '2.0'
});
req.app.proxy = true;
req.header['x-forwarded-host'] = 'proxy.com:8080';
req.header[':authority'] = 'foo.com:3000';
req.header.host = 'bar.com:8000';
assert.equal(req.host, 'proxy.com:8080');
});
});
});
});

View File

@ -1,39 +1,73 @@
var request = require('../context').request;
var assert = require('assert');
'use strict';
describe('req.hostname', function(){
it('should return hostname void of port', function(){
var req = request();
const request = require('../helpers/context').request;
const assert = require('assert');
describe('req.hostname', () => {
it('should return hostname void of port', () => {
const req = request();
req.header.host = 'foo.com:3000';
req.hostname.should.equal('foo.com');
})
assert.equal(req.hostname, 'foo.com');
});
describe('with no host present', function(){
it('should return ""', function(){
var req = request();
describe('with no host present', () => {
it('should return ""', () => {
const req = request();
assert.equal(req.hostname, '');
})
})
});
});
describe('when X-Forwarded-Host is present', function(){
describe('and proxy is not trusted', function(){
it('should be ignored', function(){
var req = request();
describe('with IPv6 in host', () => {
it('should parse localhost void of port', () => {
const req = request();
req.header.host = '[::1]';
assert.equal(req.hostname, '[::1]');
});
it('should parse localhost with port 80', () => {
const req = request();
req.header.host = '[::1]:80';
assert.equal(req.hostname, '[::1]');
});
it('should parse localhost with non special schema port', () => {
const req = request();
req.header.host = '[::1]:1337';
assert.equal(req.hostname, '[::1]');
});
it('should reduce IPv6 with non special schema port, as hostname', () => {
const req = request();
req.header.host = '[2001:cdba:0000:0000:0000:0000:3257:9652]:1337';
assert.equal(req.hostname, '[2001:cdba::3257:9652]');
});
it('should return empty string when invalid', () => {
const req = request();
req.header.host = '[invalidIPv6]';
assert.equal(req.hostname, '');
});
});
describe('when X-Forwarded-Host is present', () => {
describe('and proxy is not trusted', () => {
it('should be ignored', () => {
const req = request();
req.header['x-forwarded-host'] = 'bar.com';
req.header['host'] = 'foo.com';
req.hostname.should.equal('foo.com')
})
})
req.header.host = 'foo.com';
assert.equal(req.hostname, 'foo.com');
});
});
describe('and proxy is trusted', function(){
it('should be used', function(){
var req = request();
describe('and proxy is trusted', () => {
it('should be used', () => {
const req = request();
req.app.proxy = true;
req.header['x-forwarded-host'] = 'bar.com, baz.com';
req.header['host'] = 'foo.com';
req.hostname.should.equal('bar.com')
})
})
})
})
req.header.host = 'foo.com';
assert.equal(req.hostname, 'bar.com');
});
});
});
});

View File

@ -1,13 +1,16 @@
var Stream = require('stream');
var http = require('http');
var koa = require('../../');
var context = require('../context');
'use strict';
describe('ctx.href', function(){
it('should return the full request url', function(){
var socket = new Stream.Duplex();
var req = {
const assert = require('assert');
const Stream = require('stream');
const http = require('http');
const Koa = require('../../');
const context = require('../helpers/context');
describe('ctx.href', () => {
it('should return the full request url', () => {
const socket = new Stream.Duplex();
const req = {
url: '/users/1?next=/dashboard',
headers: {
host: 'localhost'
@ -15,34 +18,34 @@ describe('ctx.href', function(){
socket: socket,
__proto__: Stream.Readable.prototype
};
var ctx = context(req);
ctx.href.should.equal('http://localhost/users/1?next=/dashboard');
const ctx = context(req);
assert.equal(ctx.href, 'http://localhost/users/1?next=/dashboard');
// change it also work
ctx.url = '/foo/users/1?next=/dashboard';
ctx.href.should.equal('http://localhost/users/1?next=/dashboard');
})
assert.equal(ctx.href, 'http://localhost/users/1?next=/dashboard');
});
it('should work with `GET http://example.com/foo`', function(done){
var app = koa()
app.use(function* (){
this.body = this.href
})
it('should work with `GET http://example.com/foo`', done => {
const app = new Koa();
app.use(ctx => {
ctx.body = ctx.href;
});
app.listen(function(){
var address = this.address()
const address = this.address();
http.get({
host: 'localhost',
path: 'http://example.com/foo',
port: address.port
}, function(res){
res.statusCode.should.equal(200)
var buf = ''
res.setEncoding('utf8')
res.on('data', function(s){ buf += s })
res.on('end', function(){
buf.should.equal('http://example.com/foo')
done()
})
})
})
})
})
}, res => {
assert.equal(res.statusCode, 200);
let buf = '';
res.setEncoding('utf8');
res.on('data', s => buf += s);
res.on('end', () => {
assert.equal(buf, 'http://example.com/foo');
done();
});
});
});
});
});

View File

@ -1,23 +1,26 @@
var request = require('../context').request;
'use strict';
describe('ctx.idempotent', function(){
describe('when the request method is idempotent', function (){
it('should return true', function (){
const assert = require('assert');
const request = require('../helpers/context').request;
describe('ctx.idempotent', () => {
describe('when the request method is idempotent', () => {
it('should return true', () => {
['GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS', 'TRACE'].forEach(check);
function check(method) {
var req = request();
function check(method){
const req = request();
req.method = method;
req.idempotent.should.equal(true);
assert.equal(req.idempotent, true);
}
})
})
});
});
describe('when the request method is not idempotent', function(){
it('should return false', function (){
var req = request();
describe('when the request method is not idempotent', () => {
it('should return false', () => {
const req = request();
req.method = 'POST';
req.idempotent.should.equal(false);
})
})
})
assert.equal(req.idempotent, false);
});
});
});

View File

@ -1,29 +1,36 @@
var request = require('../context').request;
var assert = require('assert');
'use strict';
describe('req.inspect()', function(){
describe('with no request.req present', function(){
it('should return null', function(){
var req = request();
const request = require('../helpers/context').request;
const assert = require('assert');
const util = require('util');
describe('req.inspect()', () => {
describe('with no request.req present', () => {
it('should return null', () => {
const req = request();
req.method = 'GET';
delete req.req;
assert(null == req.inspect());
})
})
assert(undefined === req.inspect());
assert('undefined' === util.inspect(req));
});
});
it('should return a json representation', function(){
var req = request();
it('should return a json representation', () => {
const req = request();
req.method = 'GET';
req.url = 'example.com';
req.header.host = 'example.com';
req.inspect().should.eql({
const expected = {
method: 'GET',
url: 'example.com',
header: {
host: 'example.com'
}
});
})
})
};
assert.deepEqual(req.inspect(), expected);
assert.deepEqual(util.inspect(req), util.inspect(expected));
});
});

View File

@ -1,22 +1,59 @@
var request = require('../context').request;
'use strict';
describe('req.ip', function(){
describe('with req.ips present', function(){
it('should return req.ips[0]', function(){
var req = request();
req.app.proxy = true;
req.header['x-forwarded-for'] = '127.0.0.1';
const assert = require('assert');
const Stream = require('stream');
const Koa = require('../..');
const Request = require('../helpers/context').request;
describe('req.ip', () => {
describe('with req.ips present', () => {
it('should return req.ips[0]', () => {
const app = new Koa();
const req = { headers: {}, socket: new Stream.Duplex() };
app.proxy = true;
req.headers['x-forwarded-for'] = '127.0.0.1';
req.socket.remoteAddress = '127.0.0.2';
req.ip.should.equal('127.0.0.1');
})
})
const request = Request(req, undefined, app);
assert.equal(request.ip, '127.0.0.1');
});
});
describe('with no req.ips present', function(){
it('should return req.socket.removeAddress', function(){
var req = request();
describe('with no req.ips present', () => {
it('should return req.socket.remoteAddress', () => {
const req = { socket: new Stream.Duplex() };
req.socket.remoteAddress = '127.0.0.2';
req.ip.should.equal('127.0.0.2');
})
})
})
const request = Request(req);
assert.equal(request.ip, '127.0.0.2');
});
describe('with req.socket.remoteAddress not present', () => {
it('should return an empty string', () => {
const socket = new Stream.Duplex();
Object.defineProperty(socket, 'remoteAddress', {
get: () => undefined, // So that the helper doesn't override it with a reasonable value
set: () => {}
});
assert.equal(Request({ socket }).ip, '');
});
});
});
it('should be lazy inited and cached', () => {
const req = { socket: new Stream.Duplex() };
req.socket.remoteAddress = '127.0.0.2';
const request = Request(req);
assert.equal(request.ip, '127.0.0.2');
req.socket.remoteAddress = '127.0.0.1';
assert.equal(request.ip, '127.0.0.2');
});
it('should reset ip work', () => {
const req = { socket: new Stream.Duplex() };
req.socket.remoteAddress = '127.0.0.2';
const request = Request(req);
assert.equal(request.ip, '127.0.0.2');
request.ip = '127.0.0.1';
assert.equal(request.ip, '127.0.0.1');
});
});

View File

@ -1,24 +1,27 @@
var request = require('../context').request;
'use strict';
describe('req.ips', function(){
describe('when X-Forwarded-For is present', function(){
describe('and proxy is not trusted', function(){
it('should be ignored', function(){
var req = request();
const assert = require('assert');
const request = require('../helpers/context').request;
describe('req.ips', () => {
describe('when X-Forwarded-For is present', () => {
describe('and proxy is not trusted', () => {
it('should be ignored', () => {
const req = request();
req.app.proxy = false;
req.header['x-forwarded-for'] = '127.0.0.1,127.0.0.2';
req.ips.should.eql([]);
})
})
assert.deepEqual(req.ips, []);
});
});
describe('and proxy is trusted', function(){
it('should be used', function(){
var req = request();
describe('and proxy is trusted', () => {
it('should be used', () => {
const req = request();
req.app.proxy = true;
req.header['x-forwarded-for'] = '127.0.0.1,127.0.0.2';
req.ips.should.eql(['127.0.0.1', '127.0.0.2']);
})
})
})
})
assert.deepEqual(req.ips, ['127.0.0.1', '127.0.0.2']);
});
});
});
});

View File

@ -1,102 +1,103 @@
var context = require('../context');
var should = require('should');
var assert = require('assert');
'use strict';
describe('ctx.is(type)', function(){
it('should ignore params', function(){
var ctx = context();
const context = require('../helpers/context');
const assert = require('assert');
describe('ctx.is(type)', () => {
it('should ignore params', () => {
const ctx = context();
ctx.header['content-type'] = 'text/html; charset=utf-8';
ctx.header['transfer-encoding'] = 'chunked';
ctx.is('text/*').should.equal('text/html');
})
assert.equal(ctx.is('text/*'), 'text/html');
});
describe('when no body is given', function(){
it('should return null', function(){
var ctx = context();
describe('when no body is given', () => {
it('should return null', () => {
const ctx = context();
assert(null == ctx.is());
assert(null == ctx.is('image/*'));
assert(null == ctx.is('image/*', 'text/*'));
})
})
assert.equal(ctx.is(), null);
assert.equal(ctx.is('image/*'), null);
assert.equal(ctx.is('image/*', 'text/*'), null);
});
});
describe('when no content type is given', function(){
it('should return false', function(){
var ctx = context();
describe('when no content type is given', () => {
it('should return false', () => {
const ctx = context();
ctx.header['transfer-encoding'] = 'chunked';
ctx.is().should.be.false;
ctx.is('image/*').should.be.false;
ctx.is('text/*', 'image/*').should.be.false;
})
})
assert.equal(ctx.is(), false);
assert.equal(ctx.is('image/*'), false);
assert.equal(ctx.is('text/*', 'image/*'), false);
});
});
describe('give no types', function(){
it('should return the mime type', function(){
var ctx = context();
describe('give no types', () => {
it('should return the mime type', () => {
const ctx = context();
ctx.header['content-type'] = 'image/png';
ctx.header['transfer-encoding'] = 'chunked';
ctx.is().should.equal('image/png');
})
})
assert.equal(ctx.is(), 'image/png');
});
});
describe('given one type', function(){
it('should return the type or false', function(){
var ctx = context();
describe('given one type', () => {
it('should return the type or false', () => {
const ctx = context();
ctx.header['content-type'] = 'image/png';
ctx.header['transfer-encoding'] = 'chunked';
ctx.is('png').should.equal('png');
ctx.is('.png').should.equal('.png');
ctx.is('image/png').should.equal('image/png');
ctx.is('image/*').should.equal('image/png');
ctx.is('*/png').should.equal('image/png');
assert.equal(ctx.is('png'), 'png');
assert.equal(ctx.is('.png'), '.png');
assert.equal(ctx.is('image/png'), 'image/png');
assert.equal(ctx.is('image/*'), 'image/png');
assert.equal(ctx.is('*/png'), 'image/png');
ctx.is('jpeg').should.be.false;
ctx.is('.jpeg').should.be.false;
ctx.is('image/jpeg').should.be.false;
ctx.is('text/*').should.be.false;
ctx.is('*/jpeg').should.be.false;
})
})
assert.equal(ctx.is('jpeg'), false);
assert.equal(ctx.is('.jpeg'), false);
assert.equal(ctx.is('image/jpeg'), false);
assert.equal(ctx.is('text/*'), false);
assert.equal(ctx.is('*/jpeg'), false);
});
});
describe('given multiple types', function(){
it('should return the first match or false', function(){
var ctx = context();
describe('given multiple types', () => {
it('should return the first match or false', () => {
const ctx = context();
ctx.header['content-type'] = 'image/png';
ctx.header['transfer-encoding'] = 'chunked';
ctx.is('png').should.equal('png');
ctx.is('.png').should.equal('.png');
ctx.is('text/*', 'image/*').should.equal('image/png');
ctx.is('image/*', 'text/*').should.equal('image/png');
ctx.is('image/*', 'image/png').should.equal('image/png');
ctx.is('image/png', 'image/*').should.equal('image/png');
assert.equal(ctx.is('png'), 'png');
assert.equal(ctx.is('.png'), '.png');
assert.equal(ctx.is('text/*', 'image/*'), 'image/png');
assert.equal(ctx.is('image/*', 'text/*'), 'image/png');
assert.equal(ctx.is('image/*', 'image/png'), 'image/png');
assert.equal(ctx.is('image/png', 'image/*'), 'image/png');
ctx.is(['text/*', 'image/*']).should.equal('image/png');
ctx.is(['image/*', 'text/*']).should.equal('image/png');
ctx.is(['image/*', 'image/png']).should.equal('image/png');
ctx.is(['image/png', 'image/*']).should.equal('image/png');
assert.equal(ctx.is(['text/*', 'image/*']), 'image/png');
assert.equal(ctx.is(['image/*', 'text/*']), 'image/png');
assert.equal(ctx.is(['image/*', 'image/png']), 'image/png');
assert.equal(ctx.is(['image/png', 'image/*']), 'image/png');
ctx.is('jpeg').should.be.false;
ctx.is('.jpeg').should.be.false;
ctx.is('text/*', 'application/*').should.be.false;
ctx.is('text/html', 'text/plain', 'application/json; charset=utf-8').should.be.false;
})
})
assert.equal(ctx.is('jpeg'), false);
assert.equal(ctx.is('.jpeg'), false);
assert.equal(ctx.is('text/*', 'application/*'), false);
assert.equal(ctx.is('text/html', 'text/plain', 'application/json; charset=utf-8'), false);
});
});
describe('when Content-Type: application/x-www-form-urlencoded', function(){
it('should match "urlencoded"', function(){
var ctx = context();
describe('when Content-Type: application/x-www-form-urlencoded', () => {
it('should match "urlencoded"', () => {
const ctx = context();
ctx.header['content-type'] = 'application/x-www-form-urlencoded';
ctx.header['transfer-encoding'] = 'chunked';
ctx.is('urlencoded').should.equal('urlencoded');
ctx.is('json', 'urlencoded').should.equal('urlencoded');
ctx.is('urlencoded', 'json').should.equal('urlencoded');
})
})
})
assert.equal(ctx.is('urlencoded'), 'urlencoded');
assert.equal(ctx.is('json', 'urlencoded'), 'urlencoded');
assert.equal(ctx.is('urlencoded', 'json'), 'urlencoded');
});
});
});

View File

@ -1,16 +1,18 @@
var request = require('../context').request;
var assert = require('assert');
'use strict';
describe('ctx.length', function(){
it('should return length in content-length', function(){
var req = request();
const request = require('../helpers/context').request;
const assert = require('assert');
describe('ctx.length', () => {
it('should return length in content-length', () => {
const req = request();
req.header['content-length'] = '10';
req.length.should.equal(10);
})
assert.equal(req.length, 10);
});
describe('with no content-length present', function(){
var req = request();
assert(null == req.length);
})
})
it('with no content-length present', () => {
const req = request();
assert.equal(req.length, undefined);
});
});

25
test/request/origin.js Normal file
View File

@ -0,0 +1,25 @@
'use strict';
const assert = require('assert');
const Stream = require('stream');
const context = require('../helpers/context');
describe('ctx.origin', () => {
it('should return the origin of url', () => {
const socket = new Stream.Duplex();
const req = {
url: '/users/1?next=/dashboard',
headers: {
host: 'localhost'
},
socket: socket,
__proto__: Stream.Readable.prototype
};
const ctx = context(req);
assert.equal(ctx.origin, 'http://localhost');
// change it also work
ctx.url = '/foo/users/1?next=/dashboard';
assert.equal(ctx.origin, 'http://localhost');
});
});

View File

@ -1,29 +1,32 @@
var context = require('../context');
'use strict';
describe('ctx.path', function(){
it('should return the pathname', function(){
var ctx = context();
const assert = require('assert');
const context = require('../helpers/context');
describe('ctx.path', () => {
it('should return the pathname', () => {
const ctx = context();
ctx.url = '/login?next=/dashboard';
ctx.path.should.equal('/login');
})
})
assert.equal(ctx.path, '/login');
});
});
describe('ctx.path=', function(){
it('should set the pathname', function(){
var ctx = context();
describe('ctx.path=', () => {
it('should set the pathname', () => {
const ctx = context();
ctx.url = '/login?next=/dashboard';
ctx.path = '/logout';
ctx.path.should.equal('/logout');
ctx.url.should.equal('/logout?next=/dashboard');
})
assert.equal(ctx.path, '/logout');
assert.equal(ctx.url, '/logout?next=/dashboard');
});
it('should change .url but not .originalUrl', function(){
var ctx = context({ url: '/login' });
it('should change .url but not .originalUrl', () => {
const ctx = context({ url: '/login' });
ctx.path = '/logout';
ctx.url.should.equal('/logout');
ctx.originalUrl.should.equal('/login');
ctx.request.originalUrl.should.equal('/login');
})
})
assert.equal(ctx.url, '/logout');
assert.equal(ctx.originalUrl, '/login');
assert.equal(ctx.request.originalUrl, '/login');
});
});

View File

@ -1,51 +1,54 @@
var request = require('../context').request;
'use strict';
describe('req.protocol', function(){
describe('when encrypted', function(){
it('should return "https"', function(){
var req = request();
const assert = require('assert');
const request = require('../helpers/context').request;
describe('req.protocol', () => {
describe('when encrypted', () => {
it('should return "https"', () => {
const req = request();
req.req.socket = { encrypted: true };
req.protocol.should.equal('https');
})
})
assert.equal(req.protocol, 'https');
});
});
describe('when unencrypted', function(){
it('should return "http"', function(){
var req = request();
describe('when unencrypted', () => {
it('should return "http"', () => {
const req = request();
req.req.socket = {};
req.protocol.should.equal('http');
})
})
assert.equal(req.protocol, 'http');
});
});
describe('when X-Forwarded-Proto is set', function(){
describe('and proxy is trusted', function(){
it('should be used', function(){
var req = request();
describe('when X-Forwarded-Proto is set', () => {
describe('and proxy is trusted', () => {
it('should be used', () => {
const req = request();
req.app.proxy = true;
req.req.socket = {};
req.header['x-forwarded-proto'] = 'https, http';
req.protocol.should.equal('https');
})
assert.equal(req.protocol, 'https');
});
describe('and X-Forwarded-Proto is empty', function(){
it('should return "http"', function(){
var req = request();
describe('and X-Forwarded-Proto is empty', () => {
it('should return "http"', () => {
const req = request();
req.app.proxy = true;
req.req.socket = {};
req.header['x-forwarded-proto'] = '';
req.protocol.should.equal('http');
})
})
})
assert.equal(req.protocol, 'http');
});
});
});
describe('and proxy is not trusted', function(){
it('should not be used', function(){
var req = request();
describe('and proxy is not trusted', () => {
it('should not be used', () => {
const req = request();
req.req.socket = {};
req.header['x-forwarded-proto'] = 'https, http';
req.protocol.should.equal('http');
})
})
})
})
assert.equal(req.protocol, 'http');
});
});
});
});

View File

@ -1,41 +1,43 @@
var context = require('../context');
'use strict';
describe('ctx.query', function(){
describe('when missing', function(){
it('should return an empty object', function(){
var ctx = context({ url: '/' });
ctx.query.should.eql({});
})
const assert = require('assert');
const context = require('../helpers/context');
it('should return the same object each time it\'s accessed', function(done) {
var ctx = context({ url: '/' });
ctx.query.a = '2';
ctx.query.a.should.equal('2');
done();
describe('ctx.query', () => {
describe('when missing', () => {
it('should return an empty object', () => {
const ctx = context({ url: '/' });
assert.deepEqual(ctx.query, {});
});
})
it('should return a parsed query-string', function(){
var ctx = context({ url: '/?page=2' });
ctx.query.page.should.equal('2');
})
})
it('should return the same object each time it\'s accessed', () => {
const ctx = context({ url: '/' });
ctx.query.a = '2';
assert.equal(ctx.query.a, '2');
});
});
describe('ctx.query=', function(){
it('should stringify and replace the querystring and search', function(){
var ctx = context({ url: '/store/shoes' });
it('should return a parsed query-string', () => {
const ctx = context({ url: '/?page=2' });
assert.equal(ctx.query.page, '2');
});
});
describe('ctx.query=', () => {
it('should stringify and replace the querystring and search', () => {
const ctx = context({ url: '/store/shoes' });
ctx.query = { page: 2, color: 'blue' };
ctx.url.should.equal('/store/shoes?page=2&color=blue');
ctx.querystring.should.equal('page=2&color=blue');
ctx.search.should.equal('?page=2&color=blue')
})
assert.equal(ctx.url, '/store/shoes?page=2&color=blue');
assert.equal(ctx.querystring, 'page=2&color=blue');
assert.equal(ctx.search, '?page=2&color=blue');
});
it('should change .url but not .originalUrl', function(){
var ctx = context({ url: '/store/shoes' });
it('should change .url but not .originalUrl', () => {
const ctx = context({ url: '/store/shoes' });
ctx.query = { page: 2 };
ctx.url.should.equal('/store/shoes?page=2');
ctx.originalUrl.should.equal('/store/shoes');
ctx.request.originalUrl.should.equal('/store/shoes');
})
})
assert.equal(ctx.url, '/store/shoes?page=2');
assert.equal(ctx.originalUrl, '/store/shoes');
assert.equal(ctx.request.originalUrl, '/store/shoes');
});
});

View File

@ -1,30 +1,46 @@
var context = require('../context');
'use strict';
describe('ctx.querystring=', function(){
it('should replace the querystring', function(){
var ctx = context({ url: '/store/shoes' });
ctx.querystring = 'page=2&color=blue';
ctx.url.should.equal('/store/shoes?page=2&color=blue');
ctx.querystring.should.equal('page=2&color=blue');
})
const assert = require('assert');
const context = require('../helpers/context');
it('should update ctx.search and ctx.query', function(){
var ctx = context({ url: '/store/shoes' });
ctx.querystring = 'page=2&color=blue';
ctx.url.should.equal('/store/shoes?page=2&color=blue');
ctx.search.should.equal('?page=2&color=blue');
ctx.query.should.eql({
page: '2',
color: 'blue'
describe('ctx.querystring', () => {
it('should return the querystring', () => {
const ctx = context({ url: '/store/shoes?page=2&color=blue' });
assert.equal(ctx.querystring, 'page=2&color=blue');
});
describe('when ctx.req not present', () => {
it('should return an empty string', () => {
const ctx = context();
ctx.request.req = null;
assert.equal(ctx.querystring, '');
});
})
});
});
it('should change .url but not .originalUrl', function(){
var ctx = context({ url: '/store/shoes' });
describe('ctx.querystring=', () => {
it('should replace the querystring', () => {
const ctx = context({ url: '/store/shoes' });
ctx.querystring = 'page=2&color=blue';
ctx.url.should.equal('/store/shoes?page=2&color=blue');
ctx.originalUrl.should.equal('/store/shoes');
ctx.request.originalUrl.should.equal('/store/shoes');
})
})
assert.equal(ctx.url, '/store/shoes?page=2&color=blue');
assert.equal(ctx.querystring, 'page=2&color=blue');
});
it('should update ctx.search and ctx.query', () => {
const ctx = context({ url: '/store/shoes' });
ctx.querystring = 'page=2&color=blue';
assert.equal(ctx.url, '/store/shoes?page=2&color=blue');
assert.equal(ctx.search, '?page=2&color=blue');
assert.equal(ctx.query.page, '2');
assert.equal(ctx.query.color, 'blue');
});
it('should change .url but not .originalUrl', () => {
const ctx = context({ url: '/store/shoes' });
ctx.querystring = 'page=2&color=blue';
assert.equal(ctx.url, '/store/shoes?page=2&color=blue');
assert.equal(ctx.originalUrl, '/store/shoes');
assert.equal(ctx.request.originalUrl, '/store/shoes');
});
});

View File

@ -1,37 +1,38 @@
var context = require('../context');
'use strict';
describe('ctx.search=', function(){
it('should replace the search', function(){
var ctx = context({ url: '/store/shoes' });
const assert = require('assert');
const context = require('../helpers/context');
describe('ctx.search=', () => {
it('should replace the search', () => {
const ctx = context({ url: '/store/shoes' });
ctx.search = '?page=2&color=blue';
ctx.url.should.equal('/store/shoes?page=2&color=blue');
ctx.search.should.equal('?page=2&color=blue');
})
assert.equal(ctx.url, '/store/shoes?page=2&color=blue');
assert.equal(ctx.search, '?page=2&color=blue');
});
it('should update ctx.querystring and ctx.query', function(){
var ctx = context({ url: '/store/shoes' });
it('should update ctx.querystring and ctx.query', () => {
const ctx = context({ url: '/store/shoes' });
ctx.search = '?page=2&color=blue';
ctx.url.should.equal('/store/shoes?page=2&color=blue');
ctx.querystring.should.equal('page=2&color=blue');
ctx.query.should.eql({
page: '2',
color: 'blue'
assert.equal(ctx.url, '/store/shoes?page=2&color=blue');
assert.equal(ctx.querystring, 'page=2&color=blue');
assert.equal(ctx.query.page, '2');
assert.equal(ctx.query.color, 'blue');
});
it('should change .url but not .originalUrl', () => {
const ctx = context({ url: '/store/shoes' });
ctx.search = '?page=2&color=blue';
assert.equal(ctx.url, '/store/shoes?page=2&color=blue');
assert.equal(ctx.originalUrl, '/store/shoes');
assert.equal(ctx.request.originalUrl, '/store/shoes');
});
describe('when missing', () => {
it('should return ""', () => {
const ctx = context({ url: '/store/shoes' });
assert.equal(ctx.search, '');
});
})
it('should change .url but not .originalUrl', function(){
var ctx = context({ url: '/store/shoes' });
ctx.search = '?page=2&color=blue';
ctx.url.should.equal('/store/shoes?page=2&color=blue');
ctx.originalUrl.should.equal('/store/shoes');
ctx.request.originalUrl.should.equal('/store/shoes');
})
describe('when missing', function(){
it('should return ""', function(){
var ctx = context({ url: '/store/shoes' });
ctx.search.should.equal('');
})
})
})
});
});

View File

@ -1,10 +1,13 @@
var request = require('../context').request;
'use strict';
describe('req.secure', function(){
it('should return true when encrypted', function(){
var req = request();
const assert = require('assert');
const request = require('../helpers/context').request;
describe('req.secure', () => {
it('should return true when encrypted', () => {
const req = request();
req.req.socket = { encrypted: true };
req.secure.should.be.true;
})
})
assert.equal(req.secure, true);
});
});

View File

@ -1,14 +1,17 @@
var context = require('../context');
'use strict';
describe('req.stale', function(){
it('should be the inverse of req.fresh', function(){
var ctx = context();
const assert = require('assert');
const context = require('../helpers/context');
describe('req.stale', () => {
it('should be the inverse of req.fresh', () => {
const ctx = context();
ctx.status = 200;
ctx.method = 'GET';
ctx.req.headers['if-none-match'] = '"123"';
ctx.set('ETag', '"123"');
ctx.fresh.should.be.true;
ctx.stale.should.be.false;
})
})
assert.equal(ctx.fresh, true);
assert.equal(ctx.stale, false);
});
});

View File

@ -1,19 +1,28 @@
var request = require('../context').request;
'use strict';
describe('req.subdomains', function(){
it('should return subdomain array', function(){
var req = request();
const assert = require('assert');
const request = require('../helpers/context').request;
describe('req.subdomains', () => {
it('should return subdomain array', () => {
const req = request();
req.header.host = 'tobi.ferrets.example.com';
req.app.subdomainOffset = 2;
req.subdomains.should.eql(['ferrets', 'tobi']);
assert.deepEqual(req.subdomains, ['ferrets', 'tobi']);
req.app.subdomainOffset = 3;
req.subdomains.should.eql(['tobi']);
})
assert.deepEqual(req.subdomains, ['tobi']);
});
describe('with no host present', function(){
var req = request();
req.subdomains.should.eql([]);
})
})
it('should work with no host present', () => {
const req = request();
assert.deepEqual(req.subdomains, []);
});
it('should check if the host is an ip address, even with a port', () => {
const req = request();
req.header.host = '127.0.0.1:3000';
assert.deepEqual(req.subdomains, []);
});
});

View File

@ -1,16 +1,18 @@
var request = require('../context').request;
var assert = require('assert');
'use strict';
describe('req.type', function(){
it('should return type void of parameters', function(){
var req = request();
const request = require('../helpers/context').request;
const assert = require('assert');
describe('req.type', () => {
it('should return type void of parameters', () => {
const req = request();
req.header['content-type'] = 'text/html; charset=utf-8';
req.type.should.equal('text/html');
})
assert.equal(req.type, 'text/html');
});
describe('with no host present', function(){
var req = request();
assert('' === req.type);
})
})
it('with no host present', () => {
const req = request();
assert.equal(req.type, '');
});
});

View File

@ -0,0 +1,27 @@
'use strict';
const request = require('../helpers/context').request;
const assert = require('assert');
describe('req.URL', () => {
describe('should not throw when', () => {
it('host is void', () => {
// Accessing the URL should not throw.
request().URL;
});
it('header.host is invalid', () => {
const req = request();
req.header.host = 'invalid host';
// Accessing the URL should not throw.
req.URL;
});
});
it('should return empty object when invalid', () => {
const req = request();
req.header.host = 'invalid host';
assert.deepStrictEqual(req.URL, Object.create(null));
});
});

View File

@ -1,39 +1,42 @@
var context = require('../context');
'use strict';
describe('ctx.append(name, val)', function(){
it('should append multiple headers', function(){
var ctx = context();
const assert = require('assert');
const context = require('../helpers/context');
describe('ctx.append(name, val)', () => {
it('should append multiple headers', () => {
const ctx = context();
ctx.append('x-foo', 'bar1');
ctx.append('x-foo', 'bar2');
ctx.response.header['x-foo'].should.eql(['bar1', 'bar2']);
})
assert.deepEqual(ctx.response.header['x-foo'], ['bar1', 'bar2']);
});
it('should accept array of values', function (){
var ctx = context();
it('should accept array of values', () => {
const ctx = context();
ctx.append('Set-Cookie', ['foo=bar', 'fizz=buzz']);
ctx.append('Set-Cookie', 'hi=again');
ctx.response.header['set-cookie'].should.eql(['foo=bar', 'fizz=buzz', 'hi=again']);
})
assert.deepEqual(ctx.response.header['set-cookie'], ['foo=bar', 'fizz=buzz', 'hi=again']);
});
it('should get reset by res.set(field, val)', function (){
var ctx = context();
it('should get reset by res.set(field, val)', () => {
const ctx = context();
ctx.append('Link', '<http://localhost/>');
ctx.append('Link', '<http://localhost:80/>');
ctx.set('Link', '<http://127.0.0.1/>');
ctx.response.header.link.should.equal('<http://127.0.0.1/>');
})
assert.equal(ctx.response.header.link, '<http://127.0.0.1/>');
});
it('should work with res.set(field, val) first', function (){
var ctx = context();
it('should work with res.set(field, val) first', () => {
const ctx = context();
ctx.set('Link', '<http://localhost/>');
ctx.append('Link', '<http://localhost:80/>');
ctx.response.header.link.should.eql(['<http://localhost/>', '<http://localhost:80/>']);
})
})
assert.deepEqual(ctx.response.header.link, ['<http://localhost/>', '<http://localhost:80/>']);
});
});

View File

@ -1,47 +1,185 @@
var context = require('../context');
var request = require('supertest');
var koa = require('../..');
'use strict';
describe('ctx.attachment([filename])', function(){
describe('when given a filename', function(){
it('should set the filename param', function(){
var ctx = context();
const assert = require('assert');
const context = require('../helpers/context');
const request = require('supertest');
const Koa = require('../..');
describe('ctx.attachment([filename])', () => {
describe('when given a filename', () => {
it('should set the filename param', () => {
const ctx = context();
ctx.attachment('path/to/tobi.png');
var str = 'attachment; filename="tobi.png"';
ctx.response.header['content-disposition'].should.equal(str);
})
})
const str = 'attachment; filename="tobi.png"';
assert.equal(ctx.response.header['content-disposition'], str);
});
});
describe('when omitting filename', function(){
it('should not set filename param', function(){
var ctx = context();
describe('when omitting filename', () => {
it('should not set filename param', () => {
const ctx = context();
ctx.attachment();
ctx.response.header['content-disposition'].should.equal('attachment');
})
})
assert.equal(ctx.response.header['content-disposition'], 'attachment');
});
});
describe('when given a no-ascii filename', function(){
it('should set the encodeURI filename param', function(){
var ctx = context();
describe('when given a no-ascii filename', () => {
it('should set the encodeURI filename param', () => {
const ctx = context();
ctx.attachment('path/to/include-no-ascii-char-中文名-ok.png');
var str = 'attachment; filename=\"include-no-ascii-char-???-ok.png\"; filename*=UTF-8\'\'include-no-ascii-char-%E4%B8%AD%E6%96%87%E5%90%8D-ok.png';
ctx.response.header['content-disposition'].should.equal(str);
})
const str = 'attachment; filename="include-no-ascii-char-???-ok.png"; filename*=UTF-8\'\'include-no-ascii-char-%E4%B8%AD%E6%96%87%E5%90%8D-ok.png';
assert.equal(ctx.response.header['content-disposition'], str);
});
it('should work with http client', function(done){
var app = koa();
it('should work with http client', () => {
const app = new Koa();
app.use(function* (next){
this.attachment('path/to/include-no-ascii-char-中文名-ok.json')
this.body = {foo: 'bar'}
})
app.use((ctx, next) => {
ctx.attachment('path/to/include-no-ascii-char-中文名-ok.json');
ctx.body = {foo: 'bar'};
});
request(app.listen())
.get('/')
.expect('content-disposition', 'attachment; filename="include-no-ascii-char-???-ok.json"; filename*=UTF-8\'\'include-no-ascii-char-%E4%B8%AD%E6%96%87%E5%90%8D-ok.json')
.expect({foo: 'bar'})
.expect(200, done)
})
})
})
return request(app.callback())
.get('/')
.expect('content-disposition', 'attachment; filename="include-no-ascii-char-???-ok.json"; filename*=UTF-8\'\'include-no-ascii-char-%E4%B8%AD%E6%96%87%E5%90%8D-ok.json')
.expect({foo: 'bar'})
.expect(200);
});
});
});
// reference test case of content-disposition module
describe('contentDisposition(filename, options)', () => {
describe('with "fallback" option', () => {
it('should require a string or Boolean', () => {
const ctx = context();
assert.throws(() => { ctx.attachment('plans.pdf', { fallback: 42 }); },
/fallback.*string/);
});
it('should default to true', () => {
const ctx = context();
ctx.attachment('€ rates.pdf');
assert.equal(ctx.response.header['content-disposition'],
'attachment; filename="? rates.pdf"; filename*=UTF-8\'\'%E2%82%AC%20rates.pdf');
});
describe('when "false"', () => {
it('should not generate ISO-8859-1 fallback', () => {
const ctx = context();
ctx.attachment('£ and € rates.pdf', { fallback: false });
assert.equal(ctx.response.header['content-disposition'],
'attachment; filename*=UTF-8\'\'%C2%A3%20and%20%E2%82%AC%20rates.pdf');
});
it('should keep ISO-8859-1 filename', () => {
const ctx = context();
ctx.attachment('£ rates.pdf', { fallback: false });
assert.equal(ctx.response.header['content-disposition'],
'attachment; filename="£ rates.pdf"');
});
});
describe('when "true"', () => {
it('should generate ISO-8859-1 fallback', () => {
const ctx = context();
ctx.attachment('£ and € rates.pdf', { fallback: true });
assert.equal(ctx.response.header['content-disposition'],
'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', () => {
const ctx = context();
ctx.attachment('£ rates.pdf', { fallback: true });
assert.equal(ctx.response.header['content-disposition'],
'attachment; filename="£ rates.pdf"');
});
});
describe('when a string', () => {
it('should require an ISO-8859-1 string', () => {
const ctx = context();
assert.throws(() => { ctx.attachment('€ rates.pdf', { fallback: '€ rates.pdf' }); },
/fallback.*iso-8859-1/i);
});
it('should use as ISO-8859-1 fallback', () => {
const ctx = context();
ctx.attachment('£ and € rates.pdf', { fallback: '£ and EURO rates.pdf' });
assert.equal(ctx.response.header['content-disposition'],
'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', () => {
const ctx = context();
ctx.attachment('"£ rates".pdf', { fallback: '£ rates.pdf' });
assert.equal(ctx.response.header['content-disposition'],
'attachment; filename="£ rates.pdf"; filename*=UTF-8\'\'%22%C2%A3%20rates%22.pdf');
});
it('should do nothing if equal to filename', () => {
const ctx = context();
ctx.attachment('plans.pdf', { fallback: 'plans.pdf' });
assert.equal(ctx.response.header['content-disposition'],
'attachment; filename="plans.pdf"');
});
it('should use the basename of the string', () => {
const ctx = context();
ctx.attachment('€ rates.pdf', { fallback: '/path/to/EURO rates.pdf' });
assert.equal(ctx.response.header['content-disposition'],
'attachment; filename="EURO rates.pdf"; filename*=UTF-8\'\'%E2%82%AC%20rates.pdf');
});
it('should do nothing without filename option', () => {
const ctx = context();
ctx.attachment(undefined, { fallback: 'plans.pdf' });
assert.equal(ctx.response.header['content-disposition'],
'attachment');
});
});
});
describe('with "type" option', () => {
it('should default to attachment', () => {
const ctx = context();
ctx.attachment();
assert.equal(ctx.response.header['content-disposition'],
'attachment');
});
it('should require a string', () => {
const ctx = context();
assert.throws(() => { ctx.attachment(undefined, { type: 42 }); },
/invalid type/);
});
it('should require a valid type', () => {
const ctx = context();
assert.throws(() => { ctx.attachment(undefined, { type: 'invlaid;type' }); },
/invalid type/);
});
it('should create a header with inline type', () => {
const ctx = context();
ctx.attachment(undefined, { type: 'inline' });
assert.equal(ctx.response.header['content-disposition'],
'inline');
});
it('should create a header with inline type & filename', () => {
const ctx = context();
ctx.attachment('plans.pdf', { type: 'inline' });
assert.equal(ctx.response.header['content-disposition'],
'inline; filename="plans.pdf"');
});
it('should normalize type', () => {
const ctx = context();
ctx.attachment(undefined, { type: 'INLINE' });
assert.equal(ctx.response.header['content-disposition'],
'inline');
});
});
});

View File

@ -1,132 +1,134 @@
var response = require('../context').response;
var assert = require('assert');
var fs = require('fs');
'use strict';
describe('res.body=', function(){
describe('when Content-Type is set', function(){
it('should not override', function(){
var res = response();
const response = require('../helpers/context').response;
const assert = require('assert');
const fs = require('fs');
describe('res.body=', () => {
describe('when Content-Type is set', () => {
it('should not override', () => {
const res = response();
res.type = 'png';
res.body = new Buffer('something');
assert('image/png' == res.header['content-type']);
})
res.body = Buffer.from('something');
assert.equal('image/png', res.header['content-type']);
});
describe('when body is an object', function(){
it('should override as json', function(){
var res = response();
describe('when body is an object', () => {
it('should override as json', () => {
const res = response();
res.body = '<em>hey</em>';
assert('text/html; charset=utf-8' == res.header['content-type']);
assert.equal('text/html; charset=utf-8', res.header['content-type']);
res.body = { foo: 'bar' };
assert('application/json; charset=utf-8' == res.header['content-type']);
})
})
assert.equal('application/json; charset=utf-8', res.header['content-type']);
});
});
it('should override length', function(){
var res = response();
it('should override length', () => {
const res = response();
res.type = 'html';
res.body = 'something';
res.length.should.equal(9);
})
})
assert.equal(res.length, 9);
});
});
describe('when a string is given', function(){
it('should default to text', function(){
var res = response();
describe('when a string is given', () => {
it('should default to text', () => {
const res = response();
res.body = 'Tobi';
assert('text/plain; charset=utf-8' == res.header['content-type']);
})
assert.equal('text/plain; charset=utf-8', res.header['content-type']);
});
it('should set length', function(){
var res = response();
it('should set length', () => {
const res = response();
res.body = 'Tobi';
assert('4' == res.header['content-length']);
})
assert.equal('4', res.header['content-length']);
});
describe('and contains a non-leading <', function(){
it('should default to text', function(){
var res = response();
describe('and contains a non-leading <', () => {
it('should default to text', () => {
const res = response();
res.body = 'aklsdjf < klajsdlfjasd';
assert('text/plain; charset=utf-8' == res.header['content-type']);
})
})
})
assert.equal('text/plain; charset=utf-8', res.header['content-type']);
});
});
});
describe('when an html string is given', function(){
it('should default to html', function(){
var res = response();
describe('when an html string is given', () => {
it('should default to html', () => {
const res = response();
res.body = '<h1>Tobi</h1>';
assert('text/html; charset=utf-8' == res.header['content-type']);
})
assert.equal('text/html; charset=utf-8', res.header['content-type']);
});
it('should set length', function(){
var string = '<h1>Tobi</h1>';
var res = response();
it('should set length', () => {
const string = '<h1>Tobi</h1>';
const res = response();
res.body = string;
assert.equal(res.length, Buffer.byteLength(string));
})
});
it('should set length when body is overridden', function(){
var string = '<h1>Tobi</h1>';
var res = response();
it('should set length when body is overridden', () => {
const string = '<h1>Tobi</h1>';
const res = response();
res.body = string;
res.body = string + string;
assert.equal(res.length, 2 * Buffer.byteLength(string));
})
});
describe('when it contains leading whitespace', function(){
it('should default to html', function(){
var res = response();
describe('when it contains leading whitespace', () => {
it('should default to html', () => {
const res = response();
res.body = ' <h1>Tobi</h1>';
assert('text/html; charset=utf-8' == res.header['content-type']);
})
})
})
assert.equal('text/html; charset=utf-8', res.header['content-type']);
});
});
});
describe('when an xml string is given', function(){
it('should default to html', function(){
describe('when an xml string is given', () => {
it('should default to html', () => {
/**
* This test is to show that we're not going
* ctx test is to show that we're not going
* to be stricter with the html sniff
* or that we will sniff other string types.
* You should `.type=` if this simple test fails.
* You should `.type=` if ctx simple test fails.
*/
var res = response();
const res = response();
res.body = '<?xml version="1.0" encoding="UTF-8"?>\n<俄语>данные</俄语>';
assert('text/html; charset=utf-8' == res.header['content-type']);
})
})
assert.equal('text/html; charset=utf-8', res.header['content-type']);
});
});
describe('when a stream is given', function(){
it('should default to an octet stream', function(){
var res = response();
describe('when a stream is given', () => {
it('should default to an octet stream', () => {
const res = response();
res.body = fs.createReadStream('LICENSE');
assert('application/octet-stream' == res.header['content-type']);
})
})
assert.equal('application/octet-stream', res.header['content-type']);
});
});
describe('when a buffer is given', function(){
it('should default to an octet stream', function(){
var res = response();
res.body = new Buffer('hey');
assert('application/octet-stream' == res.header['content-type']);
})
describe('when a buffer is given', () => {
it('should default to an octet stream', () => {
const res = response();
res.body = Buffer.from('hey');
assert.equal('application/octet-stream', res.header['content-type']);
});
it('should set length', function(){
var res = response();
res.body = new Buffer('Tobi');
assert('4' == res.header['content-length']);
})
})
it('should set length', () => {
const res = response();
res.body = Buffer.from('Tobi');
assert.equal('4', res.header['content-length']);
});
});
describe('when an object is given', function(){
it('should default to json', function(){
var res = response();
describe('when an object is given', () => {
it('should default to json', () => {
const res = response();
res.body = { foo: 'bar' };
assert('application/json; charset=utf-8' == res.header['content-type']);
})
})
})
assert.equal('application/json; charset=utf-8', res.header['content-type']);
});
});
});

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

@ -1,30 +1,33 @@
var response = require('../context').response;
'use strict';
describe('res.etag=', function(){
it('should not modify an etag with quotes', function(){
var res = response();
const assert = require('assert');
const response = require('../helpers/context').response;
describe('res.etag=', () => {
it('should not modify an etag with quotes', () => {
const res = response();
res.etag = '"asdf"';
res.header.etag.should.equal('"asdf"');
})
assert.equal(res.header.etag, '"asdf"');
});
it('should not modify a weak etag', function(){
var res = response();
it('should not modify a weak etag', () => {
const res = response();
res.etag = 'W/"asdf"';
res.header.etag.should.equal('W/"asdf"');
})
assert.equal(res.header.etag, 'W/"asdf"');
});
it('should add quotes around an etag if necessary', function(){
var res = response();
it('should add quotes around an etag if necessary', () => {
const res = response();
res.etag = 'asdf';
res.header.etag.should.equal('"asdf"');
})
})
assert.equal(res.header.etag, '"asdf"');
});
});
describe('res.etag', function(){
it('should return etag', function(){
var res = response();
describe('res.etag', () => {
it('should return etag', () => {
const res = response();
res.etag = '"asdf"';
res.etag.should.equal('"asdf"');
})
})
assert.equal(res.etag, '"asdf"');
});
});

Some files were not shown because too many files have changed in this diff Show More