Compare commits
407 Commits
Author | SHA1 | Date |
---|---|---|
Jonatan Nilsson | dc572cd83a | |
Jonatan Nilsson | d1bcd10f82 | |
Jonatan Nilsson | 8fcdbb003c | |
Jonatan Nilsson | cd811a7046 | |
Jonatan Nilsson | 73d057a762 | |
Jonatan Nilsson | bbc03cbacd | |
Jonatan Nilsson | 969ffa59f3 | |
Jonatan Nilsson | ea8acbef1a | |
Jonatan Nilsson | 453b81ab60 | |
Jonatan Nilsson | 5a9a3f0dc2 | |
Jonatan Nilsson | 54aa155328 | |
Jonatan Nilsson | e3b8fd788f | |
Jonatan Nilsson | 141d91b216 | |
Jonatan Nilsson | 4078dc1182 | |
Jonatan Nilsson | eca102bbd3 | |
Jonatan Nilsson | 1f9d2eb86d | |
Jonatan Nilsson | 056a0e7d76 | |
Jonatan Nilsson | 450c835837 | |
Jonatan Nilsson | ca6091af55 | |
Jonatan Nilsson | 1edc7f34a8 | |
Jonatan Nilsson | 05b3cbf7d6 | |
Jonatan Nilsson | fb65d1c7ce | |
Jonatan Nilsson | 8235c2491c | |
Jonatan Nilsson | 7e7dc99e49 | |
Jonatan Nilsson | 8157e9e752 | |
Jonatan Nilsson | d655f208cb | |
Jonatan Nilsson | 6b27b844ff | |
Jonatan Nilsson | bd05c21456 | |
Jonatan Nilsson | 46b43d831b | |
Jonatan Nilsson | fad4d10057 | |
Jonatan Nilsson | dd35564df4 | |
Jonatan Nilsson | 28fee01c3f | |
Jonatan Nilsson | c9459b19ba | |
Jonatan Nilsson | 2ef7846b5f | |
fengmk2 | a0d2816cba | |
fengmk2 | 54e8fab3e3 | |
Robert Nagy | 817b498305 | |
Robert Nagy | f75d445535 | |
dead-horse | 061c21f336 | |
dead-horse | 287e589ac7 | |
dead-horse | 1015cea41d | |
Jake | 5afff89eca | |
Gunnlaugur Thor Briem | 3b23865340 | |
Jeff | a245d18a13 | |
Edvard Chen | d1d65dd29d | |
Yiyu He | 2c86b10fea | |
Peng Jie | 219bf22237 | |
Imon-Haque | 52a673703a | |
Jeff | ff70bdc75a | |
Peng Jie | b9e35469d3 | |
dead-horse | d4bdb5ed9e | |
dead-horse | 12960c437c | |
Alex Berk | 00e8f7a1b7 | |
Peng Jie | 62f29eb0c4 | |
Igor Adamenko | b7fc526ea4 | |
kzhang | 23f7f545ab | |
Dobes Vandermeer | 132c9ee63f | |
Andrew Peterson | 5810f279a4 | |
Vern Brandl | 75233d974a | |
Vern Brandl | 04e07fdc62 | |
rosald | 130e363856 | |
James George | 2f2078bf99 | |
dead-horse | 8b4e2cd3bc | |
Martin Iwanowski | b7bfa7113b | |
James George | 72f325b78e | |
call me saisai | b15115b2cb | |
dead-horse | ad91ce2346 | |
Francisco Ryan Tolmasky I | b25e79dfb5 | |
Mikhail Bodrov | d9ef60398e | |
jeremiG | 9be8583125 | |
James George | 7e46c2058c | |
Douglas Wade | 48993ade9b | |
Douglas Wade | acb388bc05 | |
Douglas Wade | a007198fa2 | |
Douglas Wade | f90e825da9 | |
Vikram Rangaraj | fc93c05f68 | |
Jordan | 5560f72912 | |
dead-horse | 281a04e8e1 | |
ZYSzys | 325792aee9 | |
André Cruz | 99051992a9 | |
Martin Iwanowski | 71aaa29591 | |
Jordan | bc81ca9414 | |
Martin Iwanowski | 0251b38a84 | |
Waleed Ashraf | 88b92b4315 | |
dead-horse | 6c0e0d6e29 | |
fengmk2 | 4964242834 | |
fengmk2 | e01cc5a1cf | |
Martin Michaelis | 9c5c58b183 | |
小雷 | 9146024e10 | |
urugator | d32623baa7 | |
dead-horse | e6853af649 | |
fengmk2 | 2ee32f50b8 | |
Clayton Ray | 2180839eda | |
dead-horse | 41257aa91e | |
dead-horse | 5fad7cd915 | |
dead-horse | 311031421f | |
Yiyu He | 0b930665a8 | |
Yiyu He | c6b8782553 | |
Yiyu He | 162a5b3e78 | |
小菜 | 74170caf0b | |
Asiel Leal | cde0bb10e9 | |
小菜 | 2cdbc52e38 | |
initial-wu | 02feadc4db | |
Ruben Bridgewater | 8f047ddb84 | |
initial-wu | 77a4cfb829 | |
Jason Macgowan | 4d42500e76 | |
initial-wu | ee1a933096 | |
initial-wu | ef33a79874 | |
initial-wu | 148f26f630 | |
Grand | 45903f228a | |
dead-horse | 45464b5174 | |
Martin Iwanowski | 2ace7044ac | |
Ruben Bridgewater | 13086d2fcd | |
Shawn Cheung | 8c17517809 | |
Martin Iwanowski | 9cef2db87e | |
Anton Harniakou | 0698d67fef | |
dead-horse | 916f914727 | |
Yiyu He | 3c23aa5b74 | |
Martin Iwanowski | 0923ef6182 | |
Joseph Lin | e544012e9f | |
Paul Anderson | a64e4ae982 | |
Tomas Ruud | e5db6a9833 | |
Mengdi Gao | c2615ecc5c | |
Alexsey | 6baa41178d | |
Nick McCurdy | 53bea20e79 | |
Michał Gołębiowski-Owczarek | 9068316fc7 | |
Nick McCurdy | 79c3c73452 | |
Mars Wong | 841844ee2f | |
jongleberry | bd89dfcafc | |
jongleberry | 418bb066af | |
jongleberry | c68a6966c2 | |
jongleberry | 687b73257b | |
Riceball LEE | 53a4446123 | |
Martin Iwanowski | 85ff544f7a | |
Bernie Stern | 6029064756 | |
Sergei Osipov | 18e4fafb02 | |
Jonas Zhang | e8a024cbc0 | |
Taehwan, No | 1e81ea3691 | |
Nick McCurdy | 43a1df8200 | |
Pedro Pablo Aste Kompen | 0168fd87a8 | |
Shawn Sit | e1e030cc3d | |
JamesWang | 77ca4290a1 | |
Martin Iwanowski | 7f577aff2d | |
Hrvoje Šimić | f3ede44ffa | |
Martin Iwanowski | 7e78147f52 | |
Todor Stoychev | 09f1b7bb1f | |
jongleberry | 86ab4ae84a | |
Clark Du | c161c0f2e9 | |
Usman Hussain | 392e8aa90d | |
Yu Qi | 78832ff6c6 | |
Rico Sta. Cruz | 7294eae078 | |
Saad Quadri | 40a6dd2c80 | |
Hartley Melamed | 0c44c11ce3 | |
Luke Bousfield | 67630217ae | |
haoxin | 9f2182dec7 | |
Chiahao Lin | 87cde82399 | |
jongleberry | aaac09af1a | |
Martin Iwanowski | 327b65cb6b | |
Martin fl0w Iwanowski | 012587889d | |
Martin fl0w Iwanowski | f6f1ab73e1 | |
Martin fl0w Iwanowski | 4b41a8b94b | |
Yiyu He | 41f8776350 | |
designgrill | beec26ebf8 | |
Kareem Kwong | 0a7856ca15 | |
Richard Marmorstein | 7941fb5221 | |
Equim | 08eb1a20c3 | |
Shaun Warman | 302814e7a3 | |
song | bfce5806c2 | |
Gilles De Mey | d394724200 | |
ziyunfei | 13c7ca6139 | |
joehecn | 9248660efd | |
TJ Holowaychuk | 32ebd1bd6a | |
Francisco Presencia | 3bbb74b3ee | |
bananaappletw | 2da3dd49fa | |
Thiago Lagden | ee5af59f1f | |
Remek Ambroziak | 7f3d0765d7 | |
George Chung | cd5d6a1c37 | |
joehecn | 1b3e08e046 | |
joehecn | 19fc4194b7 | |
Aesop Wolf | cb12aa8ae5 | |
jongleberry | 7bd82ac410 | |
Martin Iwanowski | 3721f6be0b | |
bananaappletw | efb05d2836 | |
Ivan Kleshnin | 87036857e6 | |
Fangdun Cai | 18d753ca2d | |
Michał Gołębiowski | 682d7b484a | |
jongleberry | 9a9949f9ee | |
Lee Bousfield | 55d1d9a607 | |
jongleberry | 4816cd76f0 | |
jongleberry | f841418b10 | |
jongleberry | ebed04f342 | |
Lee Bousfield | e6539e1cf2 | |
jongleberry | 65c130db5b | |
jongleberry | e812339033 | |
jongleberry | e9d7abaf79 | |
Jeff Moore | 188c0968e1 | |
fengmk2 | e452b68bd9 | |
Ilkka Oksanen | 0c28d1f57b | |
Stéphane Bisinger | 909b829ac6 | |
Ilkka Oksanen | d740d9b2b1 | |
Yiyu He | 73e9e13580 | |
jongleberry | 612a77b72b | |
石发磊 | 207aa4e75b | |
Filip Skokan | 0c2d881b67 | |
Xavier Damman | 2fda9dd0bf | |
jongleberry | 6c6aa4dab4 | |
jongleberry | 4e5deaeb9c | |
jongleberry | 65061085e4 | |
jongleberry | d4b32234ea | |
jongleberry | f191c0def4 | |
fengmk2 | a7c4236728 | |
jongleberry | cd02834f75 | |
jongleberry | 9671add57d | |
jongleberry | df7b4ff2b8 | |
Amit Portnoy | 00156dfec6 | |
Lee Bousfield | 152b6d73b7 | |
Wang Dàpéng | 8435268efc | |
dead-horse | d48291f40a | |
Rui Marinho | 7ae9c3e109 | |
iamchenxin | 2db3b1b49a | |
iamchenxin | fabf5864c6 | |
Malcolm | e3ccdac621 | |
Avindra Goolcharan | 2a16426afe | |
dead_horse | ce78786f95 | |
Adam Lau | 21c0d823dd | |
dead_horse | e4c0a53421 | |
Yiyu He | 4338cb6c14 | |
Ivan Lyons | 614b4e1ad7 | |
jongleberry | 0d7aeb1f7c | |
jongleberry | 2abed6ec75 | |
Jacob Bass | ce75a9c872 | |
dead_horse | 742a675e60 | |
dead_horse | a1cdbdafcf | |
Yiyu He | 23903e7ef4 | |
qingming | 2c507d335d | |
Martin Iwanowski | d47d0f9619 | |
Yu Qi | b16d24ecb8 | |
jongleberry | e61d54565b | |
Zack Tanner | 808768a597 | |
jongleberry | 41afac3eba | |
Yu Qi | c979056087 | |
d3v | e0a0d9f7cc | |
PlasmaPower | 54e58d3523 | |
PlasmaPower | 826ad83db6 | |
Marceli.no | fd839976e2 | |
jongleberry | be87ef8a24 | |
Yiyu He | 177b599fd5 | |
PlasmaPower | 9c6969755f | |
jongleberry | bd3d9e5100 | |
PlasmaPower | efcdd3b93e | |
jongleberry | 2b094eb895 | |
jongleberry | daf688bf69 | |
Martin Iwanowski | 0ac4ff00c6 | |
jongleberry | a1aec3d163 | |
Robin Pokorný | 340dd4f1a3 | |
dead_horse | 39f058e11c | |
jongleberry | bcada5bde9 | |
Yiyu He | 9ba2f9c5df | |
Lee Bousfield | 1d1698eb85 | |
jongleberry | 882ea7e87a | |
Jonathan Ong | 04a7122e22 | |
jongleberry | 2df468b524 | |
Jonathan Ong | 551715821b | |
Lee Bousfield | 597638ded6 | |
Lee Bousfield | a440425dc2 | |
Bartol Karuza | 3d15c2409d | |
Jonathan Ong | d768ed83b6 | |
TJ Holowaychuk | 86a6f2b380 | |
dead_horse | a808671340 | |
Prayag Verma | 53a165f543 | |
Louis DeScioli | d74802dc70 | |
jongleberry | 7373c7eca1 | |
Jonathan Ong | 82bdb8223d | |
Jonathan Ong | 5d8b759e11 | |
jongleberry | 5d330b095f | |
Jonathan Ong | dbcbc28b11 | |
jongleberry | b658fe7ca0 | |
Lee Bousfield | 6a147726bd | |
Yiyu He | 291c7c329f | |
Xiang Gao | 897ad7aca8 | |
jongleberry | 5384f10497 | |
pana | 1e38b13a94 | |
Yanick Rochon | d134fff9e8 | |
dead_horse | 69ad7ad4a5 | |
Yiyu He | 6a5c2e529a | |
Nicolae Vartolomei | 61f7c6b5c5 | |
Yiyu He | 667628d333 | |
nswbmw | aac3d70895 | |
fengmk2 | da6c63da59 | |
jongleberry | a09000357d | |
jongleberry | 65f645d341 | |
jongleberry | eb0bd4c2c3 | |
jongleberry | 51b51331ba | |
jongleberry | a6547bcbce | |
jongleberry | 09ada29881 | |
dead_horse | 848a9c885b | |
Yiyu He | 6e76e20f43 | |
dead_horse | 34e8325a39 | |
Yiyu He | b83405f052 | |
frank | 4b5ef85f29 | |
dead_horse | 08057e386a | |
jongleberry | ad6f752cff | |
broucz | 4b1a1da652 | |
TJ Holowaychuk | 5bfe0d4081 | |
Slobodan Stojanovic | 0470997854 | |
TJ Holowaychuk | 439f051776 | |
TJ Holowaychuk | 308ceee47d | |
Slobodan Stojanovic | 275356a5ce | |
blaz | 3560651bbc | |
Julian Gruber | eb03d9f61e | |
Slobodan Stojanovic | a135d932a0 | |
TJ Holowaychuk | aa1fbbff4a | |
Slobodan Stojanovic | b08facb7bd | |
Slobodan Stojanovic | 6d3b81fe50 | |
Tejas Manohar | ae9edb6dc9 | |
jongleberry | 664161a227 | |
Felix Becker | ebb4850709 | |
Tejas Manohar | ded7a17140 | |
jongleberry | 72680825d0 | |
blaz | d280122cf4 | |
Slobodan Stojanovic | 5d4df1086d | |
Slobodan Stojanovic | cc1d41f5e3 | |
broucz | e859c602d1 | |
fundon | db7da4eb4f | |
TJ Holowaychuk | 0cbe1cab6f | |
Julian Gruber | 0363608eba | |
Kwyn Alice Meagher | dce805b0ad | |
Slobodan Stojanovic | dac250b3af | |
Slobodan Stojanovic | 0df400fa60 | |
Julian Gruber | c86e3faeb1 | |
Slobodan Stojanovic | 863fce310b | |
Julian Gruber | 95a5bcd3e0 | |
pana | b6b0d02df9 | |
jongleberry | 773aed6622 | |
jongleberry | c2206a287d | |
jongleberry | 2e8cdab8bc | |
jongleberry | 16db0f60c4 | |
jongleberry | e7c72d7873 | |
Santiago Sotomayor | 0c438ed435 | |
TJ Holowaychuk | 653d7ed26d | |
Arjun | 3045b283fd | |
jongleberry | e6d76da1e5 | |
Michaël Zasso | 0a2d0dad75 | |
Michaël Zasso | b5c09a1719 | |
Michaël Zasso | a157937969 | |
Michaël Zasso | 24ccde947d | |
TJ Holowaychuk | bfa53fbc28 | |
Tejas Manohar | 132b32b287 | |
TJ Holowaychuk | c06d30286f | |
TJ Holowaychuk | 13983ede63 | |
Tejas Manohar | 46f8d49e4c | |
Tejas Manohar | 93ade5e2dd | |
Tejas Manohar | e8f79d43f9 | |
Jonathan Ong | ef467caabd | |
Jonathan Ong | 27e8edc509 | |
Robert Sköld | e900f0a44a | |
Tejas Manohar | a27781abb5 | |
Jonathan Ong | 00b3c97258 | |
Tejas Manohar | 1a55281fee | |
Tejas Manohar | a13ae6fc95 | |
Tejas Manohar | 88c35c1a0e | |
Tejas Manohar | 91ecce1d76 | |
Tejas Manohar | ed19e67055 | |
Tejas Manohar | 9f27c1c414 | |
Jonathan Ong | 07619f65c3 | |
Tejas Manohar | 96c1e0998f | |
Jonathan Ong | af0ae08dc4 | |
Tejas Manohar | 0b1b49cb8a | |
fengmk2 | f875eb0c30 | |
Tejas Manohar | 6c19c41c09 | |
TJ Holowaychuk | 65cc864c9b | |
Tejas Manohar | e717733aa8 | |
TJ Holowaychuk | 237e6c4f60 | |
TJ Holowaychuk | 65b59a9509 | |
Tejas Manohar | 10f9811e37 | |
Tejas Manohar | 5e21238594 | |
Tejas Manohar | ea4754e332 | |
Tejas Manohar | c369b33b23 | |
dead_horse | b2bcbcec7c | |
TJ Holowaychuk | 1ed691dde3 | |
TJ Holowaychuk | 2359b6a769 | |
Bryan Bess | 0192d21d73 | |
fengmk2 | 039217ad4e | |
Bryan Bess | 890244fc74 | |
Yiyu He | b3d46bd69e | |
C.T. Lin | 85860587cc | |
mdemo | 520163fe57 | |
Jonathan Ong | 9bf6bf1c07 | |
llambda | 30c8723705 | |
fengmk2 | f530219862 | |
Travis Jeffery | e710b4b05f | |
dead_horse | 6563e6ac3d | |
dead_horse | 36a933375b | |
gyson | 1be333ca31 | |
Jonathan Ong | 0b9c032af1 | |
AlexeyKhristov | 8804b7ba6f | |
Sterling Williams | 391650518f | |
dead_horse | e021a6e7cb | |
dead_horse | d3ca581ac9 | |
jongleberry | 69ed37335b | |
Matthew King | 05b5912912 | |
Julian Gruber | a8689e4e41 | |
Jingwei "John" Liu | b80007c460 | |
Yiyu He | 0ad06c9810 | |
Michaël Zasso | 0618db83c1 | |
Yiyu He | 2c9f2dcd9c | |
Yazhong Liu | 90b05c09e5 |
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
extends: koa
|
||||
rules:
|
||||
operator-linebreak: [error, before]
|
|
@ -2,3 +2,5 @@ node_modules
|
|||
test.js
|
||||
coverage
|
||||
npm-debug.log
|
||||
.idea
|
||||
*.iml
|
||||
|
|
25
.travis.yml
25
.travis.yml
|
@ -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
157
AUTHORS
|
@ -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>
|
||||
|
|
|
@ -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/
|
293
History.md
293
History.md
|
@ -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)
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -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
|
||||
|
|
50
Makefile
50
Makefile
|
@ -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
223
Readme.md
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
|
@ -1,6 +0,0 @@
|
|||
// support async await by babel
|
||||
require('babel/register')({
|
||||
optional: ['asyncToGenerator']
|
||||
});
|
||||
|
||||
require('./async');
|
|
@ -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);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
echo
|
||||
MW=$1 node --harmony $2 &
|
||||
MW=$1 USE_ASYNC=$2 node $3 &
|
||||
pid=$!
|
||||
|
||||
sleep 2
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
});
|
||||
```
|
||||
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
14
docs/faq.md
14
docs/faq.md
|
@ -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.
|
||||
|
|
199
docs/guide.md
199
docs/guide.md
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
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 |
|
@ -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.
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
```
|
|
@ -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;
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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
|
||||
}
|
|
@ -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')
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -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
|
||||
}
|
|
@ -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';
|
||||
}
|
||||
};
|
|
@ -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;
|
|
@ -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);
|
||||
};
|
282
lib/request.js
282
lib/request.js
|
@ -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;
|
||||
}
|
||||
|
|
218
lib/response.js
218
lib/response.js
|
@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
};
|
57
package.json
57
package.json
|
@ -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"
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
env:
|
||||
jest: true
|
||||
|
||||
rules:
|
||||
space-before-blocks: [2, {functions: never, keywords: always}]
|
||||
no-unused-expressions: 0
|
1084
test/application.js
1084
test/application.js
File diff suppressed because it is too large
Load Diff
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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()
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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!/);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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', () => {});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
})
|
||||
})
|
|
@ -1,4 +0,0 @@
|
|||
require('babel/register')({
|
||||
optional: ['asyncToGenerator']
|
||||
});
|
||||
require('./async');
|
|
@ -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;
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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('');
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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, []);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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, '');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
});
|
|
@ -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/>']);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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?=' }
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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
Loading…
Reference in New Issue