Repository: koajs/koa Branch: master Commit: d3ea8bf9649d Files: 108 Total size: 333.7 KB Directory structure: gitextract_l_l5wk61/ ├── .codecov.yml ├── .editorconfig ├── .github/ │ ├── FUNDING.yml │ ├── dependabot.yml │ └── workflows/ │ ├── node.js.yml │ └── npm-publish.yml ├── .gitignore ├── .mailmap ├── AUTHORS ├── CODE_OF_CONDUCT.md ├── History.md ├── LICENSE ├── Readme.md ├── __tests__/ │ ├── .eslintrc.yml │ ├── application/ │ │ ├── compose.test.js │ │ ├── context.test.js │ │ ├── currentContext.test.js │ │ ├── index.test.js │ │ ├── inspect.test.js │ │ ├── onerror.test.js │ │ ├── request.test.js │ │ ├── respond.test.js │ │ ├── response.test.js │ │ ├── toJSON.test.js │ │ └── use.test.js │ ├── context/ │ │ ├── assert.test.js │ │ ├── cookies.test.js │ │ ├── inspect.test.js │ │ ├── onerror.test.js │ │ ├── state.test.js │ │ ├── throw.test.js │ │ └── toJSON.test.js │ ├── lib/ │ │ └── search-params.test.js │ ├── load-with-esm.test.js │ ├── request/ │ │ ├── accept.test.js │ │ ├── accepts.test.js │ │ ├── acceptsCharsets.test.js │ │ ├── acceptsEncodings.test.js │ │ ├── acceptsLanguages.test.js │ │ ├── charset.test.js │ │ ├── fresh.test.js │ │ ├── get.test.js │ │ ├── header.test.js │ │ ├── headers.test.js │ │ ├── host.test.js │ │ ├── hostname.test.js │ │ ├── href.test.js │ │ ├── idempotent.test.js │ │ ├── inspect.test.js │ │ ├── ip.test.js │ │ ├── ips.test.js │ │ ├── is.test.js │ │ ├── length.test.js │ │ ├── origin.test.js │ │ ├── path.test.js │ │ ├── protocol.test.js │ │ ├── query.test.js │ │ ├── querystring.test.js │ │ ├── search.test.js │ │ ├── secure.test.js │ │ ├── stale.test.js │ │ ├── subdomains.test.js │ │ ├── type.test.js │ │ └── whatwg-url.test.js │ └── response/ │ ├── append.test.js │ ├── attachment.test.js │ ├── back.test.js │ ├── body.test.js │ ├── etag.test.js │ ├── flushHeaders.test.js │ ├── get.test.js │ ├── has.test.js │ ├── header.test.js │ ├── headers.test.js │ ├── inspect.test.js │ ├── is.test.js │ ├── last-modified.test.js │ ├── length.test.js │ ├── message.test.js │ ├── redirect.test.js │ ├── remove.test.js │ ├── set.test.js │ ├── socket.test.js │ ├── status.test.js │ ├── type.test.js │ ├── vary.test.js │ └── writable.test.js ├── docs/ │ ├── api/ │ │ ├── context.md │ │ ├── index.md │ │ ├── request.md │ │ └── response.md │ ├── error-handling.md │ ├── faq.md │ ├── guide.md │ ├── koa-vs-express.md │ ├── migration-v1-to-v2.md │ ├── migration-v2-to-v3.md │ └── troubleshooting.md ├── lib/ │ ├── application.js │ ├── context.js │ ├── is-stream.js │ ├── only.js │ ├── request.js │ ├── response.js │ └── search-params.js ├── package.json └── test-helpers/ ├── context.js └── stream.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .codecov.yml ================================================ coverage: parsers: javascript: enable_partials: yes ================================================ FILE: .editorconfig ================================================ # 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 ================================================ FILE: .github/FUNDING.yml ================================================ open_collective: koajs ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: npm directory: "/" schedule: interval: daily open-pull-requests-limit: 5 versioning-strategy: increase-if-necessary ================================================ FILE: .github/workflows/node.js.yml ================================================ name: Node.js CI on: push: branches: [master] pull_request: branches: [master] jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [18.x, 20.x, 22.x] steps: - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm ci - run: npm run lint - run: npm run test:coverage - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} ================================================ FILE: .github/workflows/npm-publish.yml ================================================ name: NPM Publish # Trigger when tags matching semver format are pushed, or manually via workflow_dispatch. # Manual triggers allow selecting a specific tag to publish (e.g. tags from the v2.x branch). # # Patterns match common semver formats: # - v1.0.0 (standard) # - v1.0.0-alpha (pre-release) # - v1.0.0-beta.1 (pre-release with number) # # Note: GitHub Actions uses glob patterns (not full regex), which limits # complex semver matching. These patterns cover most npm publishing scenarios. # For complex dotted pre-releases (v1.0.0-alpha.beta.1), use simpler formats # like v1.0.0-alphabeta1 or create the workflow manually. "on": push: tags: - 'v[0-9]+.[0-9]+.[0-9]+' - 'v[0-9]+.[0-9]+.[0-9]+-[a-zA-Z0-9]+' - 'v[0-9]+.[0-9]+.[0-9]+-[a-zA-Z0-9]+.[0-9]+' workflow_dispatch: inputs: tag: description: 'Git tag to checkout and publish (e.g. v2.15.4)' required: true type: string # Permissions for NPM trusted publishing with provenance permissions: contents: read id-token: write jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 with: ref: ${{ inputs.tag || github.ref }} - name: Setup Node.js uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 #v6 with: node-version: 22 - name: Install npm@latest run: npm install -g npm@latest - name: Install dependencies run: npm ci - name: Publish to NPM run: npm publish ================================================ FILE: .gitignore ================================================ node_modules test.js coverage npm-debug.log .idea *.iml dist ================================================ FILE: .mailmap ================================================ Michał Gołębiowski-Owczarek ================================================ FILE: AUTHORS ================================================ 小菜 Aaron Heckmann Adam L Adam Lau Aesop Wolf AlexeyKhristov Alexsey Amit Portnoy Anton Harniakou Arjun Asiel Leal Avindra Goolcharan Bartol Karuza Ben Reinhart Bernie Stern Bryan Bess C.T. Lin Chiahao Lin Chris Tarquini Christoffer Hallas Clark Du Darren Cauthon Debjeet Biswas Dmitry Mazuro Douglas Christopher Wilson Eivind Fjeldstad Equim Fangdun Cai Felix Becker Filip Skokan Francisco Presencia Gao Sheng George Chung Gilles De Mey Grand Guilherme Pacheco HanHor Wu Hartley Melamed Hrvoje Šimić Hugh Kennedy Ian Storm Taylor Ilkka Oksanen Ivan Kleshnin Ivan Lyons Jacob Bass JamesWang Jan Buschtöns Jan Carlo Viray Jason Macgowan Jed Schmidt Jeff Moore Jesus Rodriguez Jesús Rodríguez Rodríguez Jingwei "John" Liu Johan Bergström Jonas Zhang <106856363@qq.com> Jonathan Ong Jonathan Ong Joseph Lin Julian Gruber Kareem Kwong Karl Böhlmark Kenneth Ormandy Kim Joar Bekkelund Kwyn Alice Meagher Kyle Suss Lee Bousfield Louis DeScioli Luke Bousfield Malcolm Marceli.no Mars Wong Martin Iwanowski Martin Iwanowski Martin fl0w Iwanowski Matheus Azzi Mathieu Gallé-Tessonneau Matthew Chase Whittemore Matthew King Matthew Mueller Mengdi Gao Michaël Zasso Michał Gołębiowski-Owczarek Nathan Rajlich New Now Nohow Nick McCurdy Nicolae Vartolomei PatrickJS Paul Anderson Pedro Pablo Aste Kompen Peeyush Kushwaha Phillip Alexander PlasmaPower Prayag Verma Qiming zhao Remek Ambroziak Riceball LEE Richard Marmorstein Rico Sta. Cruz Robert Sköld Robin Pokorný Ruben Bridgewater Rui Marinho Rui Marinho Ryunosuke SATO Saad Quadri Santiago Sotomayor Sergei Osipov Shaun Warman Shawn Cheung <958033967@qq.com> Shawn Sit Slobodan Stojanovic Sonny Piers Sterling Williams Stéphane Bisinger TJ Holowaychuk TJ Holowaychuk Taehwan, No Tejas Manohar Teoman Soygul Thiago Lagden Tiago Ribeiro Tim Schaub Todor Stoychev Tomas Ruud Travis Jeffery Usman Hussain Veselin Todorov Wang Dàpéng Xavier Damman Xiang Gao Yanick Rochon Yazhong Liu Yazhong Liu Yiyu He Yiyu He Yoshua Wuyts Yu Qi Yu Qi Zack Tanner alsotang bananaappletw bhanuc blaz broucz d3v dead-horse dead_horse designgrill fengmk2 fengmk2 frank fundon gyson haoxin haoxin iamchenxin initial-wu jeromew joehecn jongleberry jongleberry llambda mako-taco mdemo nicoder nswbmw pana qingming <358242939@qq.com> song superchink tmilewski yoshuawuyts yosssi zensh ziyunfei <446240525@qq.com> 石发磊 ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # 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/ ================================================ FILE: History.md ================================================ > [!IMPORTANT] > Moving forwards we are using the GitHub releases page at in combination with [np](https://www.npmjs.com/package/np) for publishing releases and their changelogs. --- 3.0.0-alpha.3 / 2025-02-11 ================== **fixes** - Avoid redos on host and protocol getter 3.0.0-alpha.2 / 2024-11-04 ================== **breaking changes** - Update `http-errors` to `v2.0.0` [#1486](https://github.com/koajs/koa/pull/1486) - `ctx.throw` now requires a format of `ctx.throw(status, error, properties)`. See: https://www.npmjs.com/package/http-errors - Remove `res.redirect('back')`, add `back()` method to `ctx` [#1115](https://github.com/koajs/koa/pull/1115) - Replace node querystring with `URLSearchParams` [#1828](https://github.com/koajs/koa/pull/1828) - Remove obsolete `createAsyncCtxStorageMiddleware` [#1817](https://github.com/koajs/koa/pull/1817) **features** - Add support for web WHATWG [#1830](https://github.com/koajs/koa/pull/1830) **updates** - Update `cookies` to `~0.9.1` [#1846](https://github.com/koajs/koa/pull/1846) - Update `statuses` to `^2.0.1` - Update `supertest` to `^7.0.0` [#1841](https://github.com/koajs/koa/pull/1841) **fixes** - Fix `exports.defaults` in `package.json` [#1630](https://github.com/koajs/koa/pull/1630) - Fix leaky handles in tests [#1838](https://github.com/koajs/koa/pull/1838) - Fix body null checks [#1814](https://github.com/koajs/koa/pull/1814) - Fix reformatting redirect URLs [#1805](https://github.com/koajs/koa/pull/1805) [#1804](https://github.com/koajs/koa/pull/1804) - Fix passing `ctx` in error handler [#1758](https://github.com/koajs/koa/pull/1758) **migrations** - Migrate from `jest` to the native node test runner [#1845](https://github.com/koajs/koa/pull/1845) 3.0.0-alpha.1 / 2023-04-12 ================== **fixes** * [[`e98b8d1`](http://github.com/koajs/koa/commit/e98b8d1918376dc2957aa62906bf5893bef66c4c)] - fix: can not get currentContext in error handler (#1758) (Gxkl <>) 3.0.0-alpha.0 / 2023-01-02 ================== ## Breaking Changes - Supports node@12+ only. - Removes generator deprecation messages. Generators are no longer supported. Koa no longer asserts if generators are used. - Set `content-length: 0` if body is explicitly set to `null` @ognjenjevremovic #1528 ## Features - Use asyncLocalStorage to get current context from app, e.g.: `const ctx = app.currentContext`. ## Fixes - fix: Do not response Content-Length if Transfer-Encoding is defined #1562 @charlyzeng - fix: Set body to `null` if `ctx.type = json` and `ctx.body = null` #1059 @likegun 2.13.1 / 2021-01-04 ================== **fixes** * [[`b5472f4`](http://github.com/koajs/koa/commit/b5472f4cbb87349becae36b4a9ad5f76a825abb8)] - fix: make ESM transpiled CommonJS play nice for TS folks, fix #1513 (#1518) (miwnwski <>) * [[`68d97d6`](http://github.com/koajs/koa/commit/68d97d69e4536065504bf9ef1e348a66b3f35709)] - fix: fixed order of vulnerability disclosure addresses (niftylettuce <>) **others** * [[`b4398f5`](http://github.com/koajs/koa/commit/b4398f5d68f9546167419f394a686afdcb5e10e2)] - correct verb tense in doc (#1512) (Matan Shavit <<71092861+matanshavit@users.noreply.github.com>>) * [[`39e1a5a`](http://github.com/koajs/koa/commit/39e1a5a380aa2bbc4e2d164e8e4bf37cfd512516)] - fixed multiple grammatical errors in docs. (#1497) (Hridayesh Sharma <>) * [[`aeb5d19`](http://github.com/koajs/koa/commit/aeb5d1984dcc5f8e3386f8f9724807ae6f3aa1c4)] - docs: added niftylettuce@gmail.com to vulnerability disclosure (niftylettuce <>) * [[`6e1093b`](http://github.com/koajs/koa/commit/6e1093be27b41135c8e67fce108743d54e9cab67)] - docs: remove babel from readme (#1494) (miwnwski <>) * [[`38cb591`](http://github.com/koajs/koa/commit/38cb591254ff5f65a04e8fb57be293afe697c46e)] - docs: update specific for auto response status (AlbertAZ1992 <>) * [[`2224cd9`](http://github.com/koajs/koa/commit/2224cd9b6a648e7ac2eb27eac332e7d6de7db26c)] - docs: remove babel ref. (#1488) (Imed Jaberi <>) * [[`d51f983`](http://github.com/koajs/koa/commit/d51f98328c3b84493cc6bda0732aabb69e20e3a1)] - docs: fix assert example for response (#1489) (Imed Jaberi <>) * [[`f8b49b8`](http://github.com/koajs/koa/commit/f8b49b859363ad6c3d9ea5c11ee62341407ceafd)] - chore: fix grammatical and spelling errors in comments and tests (#1490) (Matt Kubej <>) * [[`d1c9263`](http://github.com/koajs/koa/commit/d1c92638c95d799df2fdff5576b96fc43a62813f)] - deps: update depd >> v2.0.0 (#1482) (imed jaberi <>) 2.13.0 / 2020-06-21 ================== **features** * [[`bbcde76`](http://github.com/koajs/koa/commit/bbcde76f5cb5b67bbcd3201791cf0ef648fd3a8b)] - feat: support esm (#1474) (ZYSzys <>) **others** * [[`20e58cf`](http://github.com/koajs/koa/commit/20e58cf3e4f20fc5d5886df1d0ac6dd8c33bd202)] - test: imporve coverage to 100% (dead-horse <>) * [[`4a40d63`](http://github.com/koajs/koa/commit/4a40d633c4b4a203c6656078f9952ccef65c5875)] - build: use prepare instead of prepublish (dead-horse <>) * [[`226ba8c`](http://github.com/koajs/koa/commit/226ba8c8e81e83da48e7bf137be3f146d03f40b8)] - build: use prepublish instead of prepack (dead-horse <>) 2.12.1 / 2020-06-13 ================== **fixes** * [[`e2030c7`](http://github.com/koajs/koa/commit/e2030c7249c7ae24e28158d8eae405a02fefc9f8)] - fix: Improve checks for Error in onerror handlers (#1468) (Julien Wajsberg <>) **others** * [[`5208c5e`](http://github.com/koajs/koa/commit/5208c5e15d35b3653fce6b8ed68d09865abea843)] - chore: Use single console.error() statement in error handler (#1471) (Mike Vosseller <>) 2.12.0 / 2020-05-18 ================== **features** * [[`0d2f421`](http://github.com/koajs/koa/commit/0d2f421c265350d3d84e1bc261572954479f27d3)] - feat: error handler treat err.statusCode as the same as err.status (#1460) (Vijay Krishnavanshi <>) * [[`8d52105`](http://github.com/koajs/koa/commit/8d52105a34234be9e771ff3b76b43e4e30328943)] - feat: allow bodyless responses for non empty status codes (#1447) (ejose19 <<8742215+ejose19@users.noreply.github.com>>) **others** * [[`faeaff5`](http://github.com/koajs/koa/commit/faeaff5c149a81a188ab8e5af0b994029e45acbb)] - fox: remove `error-inject` and fix error handling (#1409) (Konstantin Vyatkin <>) * [[`f7c732f`](http://github.com/koajs/koa/commit/f7c732fd06f724505e9090add4d977e667da55a8)] - docs: fixed incorrect onerror example (#1459) (Paul Annekov <>) * [[`143d8f7`](http://github.com/koajs/koa/commit/143d8f72f2a232b4c97eac00e7811015911e4f7c)] - Always use strict equality. (#1225) (Yazan Medanat <>) * [[`6b6b0dd`](http://github.com/koajs/koa/commit/6b6b0ddf7aff073e65493c6efaffab8331c0331c)] - docs(api): add app.use chainability note (#1449) (Zac Anger <>) * [[`8ddab48`](http://github.com/koajs/koa/commit/8ddab48cbdbca1e6d1cc8c3ddae45491db524d51)] - docs: Document response status with empty body (#1445) (Marc-Aurèle DARCHE <<152407+madarche@users.noreply.github.com>>) * [[`7deedb2`](http://github.com/koajs/koa/commit/7deedb235274223f1b9da46dee296545b23598de)] - docs: Updating context.md with the latest cookies opts (#1433) (Brad Ito <>) * [[`3e97a10`](http://github.com/koajs/koa/commit/3e97a106bb846d9337737011bb85149ddd797229)] - docs(links): remove Google+ link (#1439) (laffachan <<45162759+laffachan@users.noreply.github.com>>) * [[`eda2760`](http://github.com/koajs/koa/commit/eda27608f7d39ede86d7b402aae64b1867ce31c6)] - build: Drop unused Travis sudo: false directive (#1416) (Olle Jonsson <>) 2.11.0 / 2019-10-28 ================== **features** * [[`422e539`](http://github.com/koajs/koa/commit/422e539e8989e65ba43ecc39ddbaa3c4f755d465)] - feat: support app.proxyIPHeader and app.maxIpsCount to make ctx.ips more security (Yiyu He <>) * [[`d48d88e`](http://github.com/koajs/koa/commit/d48d88ee17b780c02123e6d657274cab456e943e)] - feat: implement response.has (#1397) (Konstantin Vyatkin <>) **others** * [[`4dc56f6`](http://github.com/koajs/koa/commit/4dc56f6d04e8f5fe12ba53a8a776653b3d7b60ed)] - chore: update ESLint and plugins/configs (#1407) (Konstantin Vyatkin <>) * [[`be7d334`](http://github.com/koajs/koa/commit/be7d334778481639294cdf87f5c359a230aeb65b)] - chore: removes code duplication at handling HEAD method (#1400) (Konstantin Vyatkin <>) * [[`f155785`](http://github.com/koajs/koa/commit/f155785e2bb42b5ddf0a8156401c6dafdf57ba8b)] - chore: support `writableEnded` (#1402) (Konstantin Vyatkin <>) * [[`b968688`](http://github.com/koajs/koa/commit/b968688afe2c727ae141f50aa983d481dbc1dbbf)] - chore: add FUNDING.yml (#1403) (Konstantin Vyatkin <>) * [[`4f96829`](http://github.com/koajs/koa/commit/4f968298f97394e488297ec32c8e927a3a322076)] - chore: remove isJSON in res.length (#1399) (Konstantin Vyatkin <>) * [[`8be5626`](http://github.com/koajs/koa/commit/8be5626bbb54e6c899a1b71d22411709126d9fea)] - build: enable codecov partial coverage and use bash uploader (#1396) (Konstantin Vyatkin <>) * [[`ef5c43b`](http://github.com/koajs/koa/commit/ef5c43bcbcf31819e032c3b7ae7654b7f8e9358b)] - chore: use rest params (#1393) (Konstantin Vyatkin <>) 2.10.0 / 2019-10-12 ================== **features** * [[`d7f7f77`](http://github.com/koajs/koa/commit/d7f7f77689e2eaef050686be2bdf3e72881a79ac)] - feat: support sameSite=none cookies (bump cookies dependency) (#1390) (Filip Skokan <>) 2.9.0 / 2019-10-12 ================== **features** * [[`2d1c598`](http://github.com/koajs/koa/commit/2d1c5981869e0fe6f5bc71b5c5582accfd125cc6)] - feat: export HttpError from http-errors library (Micheal Hill <>) **others** * [[`cf70dbc`](http://github.com/koajs/koa/commit/cf70dbc6d2ba62bf1eb12b563dd5ecd27af6e2be)] - Chore: Use https in readme (#1389) (谭九鼎 <<109224573@qq.com>>) 2.8.2 / 2019-09-28 ================== **fixes** * [[`54e8fab`](http://github.com/koajs/koa/commit/54e8fab3e3d907bbb264caf3e28a24773d0d6fdb)] - fix: encode redirect url if not already encoded (#1384) (fengmk2 <>) **others** * [[`817b498`](http://github.com/koajs/koa/commit/817b49830571b45a8aec6b1fc1525434f5798c58)] - test: fix body test (#1375) (Robert Nagy <>) * [[`f75d445`](http://github.com/koajs/koa/commit/f75d4455359ecdf30eeb676e2c7f31d4cf7b42ed)] - test: fix end after end (#1374) (Robert Nagy <>) 2.8.1 / 2019-08-19 ================== **fixes** * [[`287e589`](http://github.com/koajs/koa/commit/287e589ac773d3738b2aa7d40e0b6d43dde5261b)] - fix: make options more compatibility (dead-horse <>) 2.8.0 / 2019-08-19 ================== **features** * [[`5afff89`](http://github.com/koajs/koa/commit/5afff89eca0efe7081309dc2d123309e825df221)] - feat: accept options in the Application constructor (#1372) (Jake <>) **fixes** * [[`ff70bdc`](http://github.com/koajs/koa/commit/ff70bdc75a30a37f63fc1f7d8cbae3204df3d982)] - fix: typo on document (#1355) (Jeff <>) **others** * [[`3b23865`](http://github.com/koajs/koa/commit/3b23865340cfba075f61f7dba0ea31fcc27260ec)] - docs: parameter of request.get is case-insensitive (#1373) (Gunnlaugur Thor Briem <>) * [[`a245d18`](http://github.com/koajs/koa/commit/a245d18a131341feec4f87659746954e78cae780)] - docs: Update response.socket (#1357) (Jeff <>) * [[`d1d65dd`](http://github.com/koajs/koa/commit/d1d65dd29d7bbaf9ea42eaa5fcb0da3fb4df98e9)] - chore(deps): install egg-bin, mm as devDeps not deps (#1366) (Edvard Chen <>) * [[`2c86b10`](http://github.com/koajs/koa/commit/2c86b10feafd868ebd071dda3a222e6f51972b5d)] - test: remove jest and use egg-bin(mocha) (#1363) (Yiyu He <>) * [[`219bf22`](http://github.com/koajs/koa/commit/219bf22237b11bc375e2e110b93db512f1acfdd4)] - docs(context): update link (#1354) (Peng Jie <>) * [[`52a6737`](http://github.com/koajs/koa/commit/52a673703a87a93c0f6a8552e6bd73caba66d2eb)] - chore: ignore Intellij IDEA project files (#1361) (Imon-Haque <<38266345+Imon-Haque@users.noreply.github.com>>) * [[`b9e3546`](http://github.com/koajs/koa/commit/b9e35469d3bbd0a1ee92e0a815ce2512904d4a18)] - docs(api): fix keygrip link (#1350) (Peng Jie <>) * [[`d4bdb5e`](http://github.com/koajs/koa/commit/d4bdb5ed9e2fe06ec44698b66c029f624135a0ab)] - chore: update eslint and fix lint errors (dead-horse <>) * [[`12960c4`](http://github.com/koajs/koa/commit/12960c437cc25c53e682cfe5bff06d74a5bb1eb9)] - build: test on 8/10/12 (dead-horse <>) * [[`00e8f7a`](http://github.com/koajs/koa/commit/00e8f7a1b7603aabdb7fb3567f485cb1c2076702)] - docs: ctx.type aliases ctx.response, not ctx.request (#1343) (Alex Berk <>) * [[`62f29eb`](http://github.com/koajs/koa/commit/62f29eb0c4dee01170a5511615e5bcc9faca26ca)] - docs(context): update cookies link (#1348) (Peng Jie <>) * [[`b7fc526`](http://github.com/koajs/koa/commit/b7fc526ea49894f366153bd32997e02568c0b8a6)] - docs: fix typo in cookie path default value docs (#1340) (Igor Adamenko <>) * [[`23f7f54`](http://github.com/koajs/koa/commit/23f7f545abfe1fb6499cd61cc8ff41fd86cef4a0)] - chore: simplify variable (#1332) (kzhang <>) * [[`132c9ee`](http://github.com/koajs/koa/commit/132c9ee63f92a586a120ed3bd6b7ef023badb8bb)] - docs: Clarify the format of request.headers (#1325) (Dobes Vandermeer <>) * [[`5810f27`](http://github.com/koajs/koa/commit/5810f279a4caeda115f39e429c9671795613abf8)] - docs: Removed Document in Progress note in Koa vs Express (#1336) (Andrew Peterson <>) * [[`75233d9`](http://github.com/koajs/koa/commit/75233d974a30af6e3b8ab38a73e5ede67172fc1c)] - chore: Consider removing this return statement; it will be ignored. (#1322) (Vern Brandl <>) * [[`04e07fd`](http://github.com/koajs/koa/commit/04e07fdc620841068f12b8edf36f27e6592a0a18)] - test: Buffer() is deprecated due to security and usability issues. so use the Buffer.alloc() instead (#1321) (Vern Brandl <>) * [[`130e363`](http://github.com/koajs/koa/commit/130e363856747b487652f04b5550056d7778e43a)] - docs: use 'fs-extra' instead of 'fs-promise' (#1309) (rosald <<35028438+rosald@users.noreply.github.com>>) * [[`2f2078b`](http://github.com/koajs/koa/commit/2f2078bf998bd3f44289ebd17eeccf5e12e4c134)] - chore: Update PR-welcome badge url (#1299) (James George <>) 2.7.0 / 2019-01-28 ================== **features** * [[`b7bfa71`](http://github.com/koajs/koa/commit/b7bfa7113b8d1af49a57ab767f24a599ed92044f)] - feat: change set status assert, allowing valid custom statuses (#1308) (Martin Iwanowski <>) **others** * [[`72f325b`](http://github.com/koajs/koa/commit/72f325b78edd0dc2aac940a76ce5f644005ce4c3)] - chore: add pr welcoming badge (#1291) (James George <>) * [[`b15115b`](http://github.com/koajs/koa/commit/b15115b2cbfffe15827cd5e4368267d417b72f08)] - chore: Reduce unnecessary variable declarations (#1298) (call me saisai <<1457358080@qq.com>>) * [[`ad91ce2`](http://github.com/koajs/koa/commit/ad91ce2346cb34e5d5a49d07dd952d15f6c832a3)] - chore: license 2019 (dead-horse <>) * [[`b25e79d`](http://github.com/koajs/koa/commit/b25e79dfb599777a38157bd419395bd28369ee86)] - Mark two examples as live for the corresponding documentation change in https://github.com/koajs/koajs.com/pull/38. (#1031) (Francisco Ryan Tolmasky I <>) * [[`d9ef603`](http://github.com/koajs/koa/commit/d9ef60398e88f2c2f958ab2b159d38052ffe7f8a)] - chore: Optimize array split (#1295) (Mikhail Bodrov <>) * [[`9be8583`](http://github.com/koajs/koa/commit/9be858312553002841725b617050aaff3c48951d)] - chore: replace ~~ with Math.trunc in res.length (option) (#1288) (jeremiG <>) * [[`7e46c20`](http://github.com/koajs/koa/commit/7e46c2058cb5994809eab5f4dbb12f21e937c72b)] - docs: add link to the license file (#1290) (James George <>) * [[`48993ad`](http://github.com/koajs/koa/commit/48993ade9b0831fbce28d94b3b0963a4b0dccbdd)] - docs: Document other body types (#1285) (Douglas Wade <>) * [[`acb388b`](http://github.com/koajs/koa/commit/acb388bc0546b48fca11dce8aa7a595af2cda5e2)] - docs: Add security vulnerability disclosure instructions to the Readme (#1283) (Douglas Wade <>) * [[`a007198`](http://github.com/koajs/koa/commit/a007198fa23c19902b1f3ffb81498629e0e9c875)] - docs: Document ctx.app.emit (#1284) (Douglas Wade <>) * [[`f90e825`](http://github.com/koajs/koa/commit/f90e825da9d505c11b4262c50cd54553f979c300)] - docs: response.set(fields) won't overwrites previous header fields(#1282) (Douglas Wade <>) * [[`fc93c05`](http://github.com/koajs/koa/commit/fc93c05f68398f30abc46fd16ae6c673a1eee099)] - docs: update readme to add babel 7 instructions (#1274) (Vikram Rangaraj <>) * [[`5560f72`](http://github.com/koajs/koa/commit/5560f729124f022ffed00085aafea43dded7fb03)] - chore: use the ability of `content-type` lib directly (#1276) (Jordan <>) 2.6.2 / 2018-11-10 ================== **fixes** * [[`9905199`](http://github.com/koajs/koa/commit/99051992a9f45eb0dd79e062681d6f5d366deb41)] - fix: Status message is not supported on HTTP/2 (#1264) (André Cruz <>) **others** * [[`325792a`](http://github.com/koajs/koa/commit/325792aee92de0ba6fea306657933fc63dc00474)] - docs: add table of contents for guide.md (#1267) (ZYSzys <>) * [[`71aaa29`](http://github.com/koajs/koa/commit/71aaa29591d6681f8579486f18d32ba1ee651a5b)] - docs: fix spelling in throw docs (#1269) (Martin Iwanowski <>) * [[`bc81ca9`](http://github.com/koajs/koa/commit/bc81ca9414296234c764b7306a19ba72b2e59b52)] - chore: use res instead of this.res (#1271) (Jordan <>) * [[`0251b38`](http://github.com/koajs/koa/commit/0251b38a8405471892c5eeaba7c8d54bd7028214)] - test: node v11 on travis (#1265) (Martin Iwanowski <>) * [[`88b92b4`](http://github.com/koajs/koa/commit/88b92b43153f21609aee71d47abcd4dc27a6586d)] - doc: updated docs for throw() to pass status as first param. (#1268) (Waleed Ashraf <>) 2.6.1 / 2018-10-23 ================== **fixes** * [[`4964242`](http://github.com/koajs/koa/commit/49642428342e5f291eb9d690802e83ed830623b5)] - fix: use X-Forwarded-Host first on app.proxy present (#1263) (fengmk2 <>) 2.6.0 / 2018-10-23 ================== **features** * [[`9c5c58b`](http://github.com/koajs/koa/commit/9c5c58b18363494976185e7ddc790ac63de840ed)] - feat: use :authority header of http2 requests as host (#1262) (Martin Michaelis <>) * [[`9146024`](http://github.com/koajs/koa/commit/9146024e1094e8bb871ab15d1b7fc556a710732f)] - feat: response.attachment append a parameter: options from contentDisposition (#1240) (小雷 <<863837949@qq.com>>) **others** * [[`d32623b`](http://github.com/koajs/koa/commit/d32623baa7a6273d47be67d587ad4ea0ecffc5de)] - docs: Update error-handling.md (#1239) (urugator <>) 2.5.3 / 2018-09-11 ================== **fixes** * [[`2ee32f5`](http://github.com/koajs/koa/commit/2ee32f50b88b383317e33cc0a4bfaa5f2eadead7)] - fix: pin debug@~3.1.0 avoid deprecated warnning (#1245) (fengmk2 <>) **others** * [[`2180839`](http://github.com/koajs/koa/commit/2180839eda2cb16edcfda46ccfe24711680af850)] - docs: Update koa-vs-express.md (#1230) (Clayton Ray <>) 2.5.2 / 2018-07-12 ================== * deps: upgrade all dependencies * perf: avoid stringify when set header (#1220) * perf: cache content type's result (#1218) * perf: lazy init cookies and ip when first time use it (#1216) * chore: fix comment & approve cov (#1214) * docs: fix grammar * test&cov: add test case (#1211) * Lazily initialize `request.accept` and delegate `context.accept` (#1209) * fix: use non deprecated custom inspect (#1198) * Simplify processes in the getter `request.protocol` (#1203) * docs: better demonstrate middleware flow (#1195) * fix: Throw a TypeError instead of a AssertionError (#1199) * chore: mistake in a comment (#1201) * chore: use this.res.socket insteadof this.ctx.req.socket (#1177) * chore: Using "listenerCount" instead of "listeners" (#1184) 2.5.1 / 2018-04-27 ================== * test: node v10 on travis (#1182) * fix tests: remove unnecessary assert doesNotThrow and api calls (#1170) * use this.response insteadof this.ctx.response (#1163) * deps: remove istanbul (#1151) * Update guide.md (#1150) 2.5.0 / 2018-02-11 ================== * feat: ignore set header/status when header sent (#1137) * run coverage using --runInBand (#1141) * [Update] license year to 2018 (#1130) * docs: small grammatical fix in api docs index (#1111) * docs: fixed typo (#1112) * docs: capitalize K in word koa (#1126) * Error handling: on non-error throw try to stringify if error is an object (#1113) * Use eslint-config-koa (#1105) * Update mgol's name in AUTHORS, add .mailmap (#1100) * Avoid generating package locks instead of ignoring them (#1108) * chore: update copyright year to 2017 (#1095) 2.4.1 / 2017-11-06 ================== * fix bad merge w/ 2.4.0 2.4.0 / 2017-11-06 ================== UNPUBLISHED * update `package.engines.node` to be more strict * update `fresh@^0.5.2` * fix: `inspect()` no longer crashes `context` * fix: gated `res.statusMessage` for HTTP/2 * added: `app.handleRequest()` is exposed 2.3.0 / 2017-06-20 ================== * fix: use `Buffer.from()` * test on node 7 & 8 * add `package-lock.json` to `.gitignore` * run `lint --fix` * add `request.header` in addition to `request.headers` * add IPv6 hostname support 2.2.0 / 2017-03-14 ================== * fix: drop `package.engines.node` requirement to >= 6.0.0 * this fixes `yarn`, which errors when this semver range is not satisfied * bump `cookies@~0.7.0` * bump `fresh@^0.5.0` 2.1.0 / 2017-03-07 ================== * added: return middleware chain promise from `callback()` #848 * added: node v7.7+ `res.getHeaderNames()` support #930 * added: `err.headerSent` in error handling #919 * added: lots of docs! 2.0.1 / 2017-02-25 ================== NOTE: we hit a versioning snafu. `v2.0.0` was previously released, so `v2.0.1` is released as the first `v2.x` with a `latest` tag. * upgrade mocha #900 * add names to `application`'s request and response handlers #805 * breaking: remove unused `app.name` #899 * breaking: drop official support for node < 7.6 2.0.0 / ?????????? ================== * Fix malformed content-type header causing exception on charset get (#898) * fix: subdomains should be [] if the host is an ip (#808) * don't pre-bound onerror [breaking change] (#800) * fix `ctx.flushHeaders()` to use `res.flushHeaders()` instead of `res.writeHead()` (#795) * fix(response): correct response.writable logic (#782) * merge v1.1.2 and v1.2.0 changes * include `koa-convert` so that generator functions still work * NOTE: generator functions are deprecated in v2 and will be removed in v3 * improve linting * improve docs 2.0.0-alpha.8 / 2017-02-13 ================== * Fix malformed content-type header causing exception on charset get (#898) 2.0.0-alpha.7 / 2016-09-07 ================== * fix: subdomains should be [] if the host is an ip (#808) 2.0.0-alpha.6 / 2016-08-29 ================== * don't pre-bound onerror [breaking change] 2.0.0-alpha.5 / 2016-08-10 ================== * fix `ctx.flushHeaders()` to use `res.flushHeaders()` instead of `res.writeHead()` 2.0.0-alpha.4 / 2016-07-23 ================== * fix `response.writeable` during pipelined requests 1.2.0 / 2016-03-03 ================== * add support for `err.headers` in `ctx.onerror()` - see: https://github.com/koajs/koa/pull/668 - note: you should set these headers in your custom error handlers as well - docs: https://github.com/koajs/koa/blob/master/docs/error-handling.md * fix `cookies`' detection of http/https - see: https://github.com/koajs/koa/pull/614 * deprecate `app.experimental = true`. Koa v2 does not use this signature. * add a code of conduct * test against the latest version of node * add a lot of docs 1.1.2 / 2015-11-05 ================== * ensure parseurl always working as expected * fix Application.inspect() – missing .proxy value. 2.0.0-alpha.3 / 2015-11-05 ================== * ensure parseurl always working as expected. #586 * fix Application.inspect() – missing .proxy value. Closes #563 2.0.0-alpha.2 / 2015-10-27 ================== * remove `co` and generator support completely * improved documentation * more refactoring into ES6 2.0.0-alpha.1 / 2015-10-22 ================== * change the middleware signature to `async (ctx, next) => await next()` * drop node < 4 support and rewrite the codebase in ES6 1.1.1 / 2015-10-22 ================== * do not send a content-type when the type is unknown #536 1.1.0 / 2015-10-11 ================== * add `app.silent=` to toggle error logging @tejasmanohar #486 * add `ctx.origin` @chentsulin #480 * various refactoring - add `use strict` everywhere 1.0.0 / 2015-08-22 ================== * add `this.req` check for `querystring()` * don't log errors with `err.expose` * `koa` now follows semver! 0.21.0 / 2015-05-23 ================== * empty `request.query` objects are now always the same instance * bump `fresh@0.3.0` 0.20.0 / 2015-04-30 ================== Breaking change if you're using `this.get('ua') === undefined` etc. For more details please checkout [#438](https://github.com/koajs/koa/pull/438). * make sure helpers return strict string * feat: alias response.headers to response.header 0.19.1 / 2015-04-14 ================== * non-error thrown, fixed #432 0.19.0 / 2015-04-05 ================== * `req.host` and `req.hostname` now always return a string (semi-breaking change) * improved test coverage 0.18.1 / 2015-03-01 ================== * move babel to `devDependencies` 0.18.0 / 2015-02-14 ================== * experimental es7 async function support via `app.experimental = true` * use `content-type` instead of `media-typer` 0.17.0 / 2015-02-05 ================== Breaking change if you're using an old version of node v0.11! Otherwise, you should have no trouble upgrading. * official iojs support * drop support for node.js `>= 0.11.0 < 0.11.16` * use `Object.setPrototypeOf()` instead of `__proto__` * update dependencies 0.16.0 / 2015-01-27 ================== * add `res.append()` * fix path usage for node@0.11.15 0.15.0 / 2015-01-18 ================== * add `this.href` 0.14.0 / 2014-12-15 ================== * remove `x-powered-by` response header * fix the content type on plain-text redirects * add ctx.state * bump `co@4` * bump dependencies 0.13.0 / 2014-10-17 ================== * add this.message * custom status support via `statuses` 0.12.2 / 2014-09-28 ================== * use wider semver ranges for dependencies koa maintainers also maintain 0.12.1 / 2014-09-21 ================== * bump content-disposition * bump statuses 0.12.0 / 2014-09-20 ================== * add this.assert() * use content-disposition 0.11.0 / 2014-09-08 ================== * fix app.use() assertion #337 * bump a lot of dependencies 0.10.0 / 2014-08-12 ================== * add `ctx.throw(err, object)` support * add `ctx.throw(err, status, object)` support 0.9.0 / 2014-08-07 ================== * add: do not set `err.expose` to true when err.status not a valid http status code * add: alias `request.headers` as `request.header` * add context.inspect(), cleanup app.inspect() * update cookies * fix `err.status` invalid lead to uncaughtException * fix middleware gif, close #322 0.8.2 / 2014-07-27 ================== * bump co * bump parseurl 0.8.1 / 2014-06-24 ================== * bump type-is 0.8.0 / 2014-06-13 ================== * add `this.response.is()`` * remove `.status=string` and `res.statusString` #298 0.7.0 / 2014-06-07 ================== * add `this.lastModified` and `this.etag` as both getters and setters for ubiquity #292. See koajs/koa@4065bf7 for an explanation. * refactor `this.response.vary()` to use [vary](https://github.com/expressjs/vary) #291 * remove `this.response.append()` #291 0.6.3 / 2014-06-06 ================== * fix res.type= when the extension is unknown * assert when non-error is passed to app.onerror #287 * bump finished 0.6.2 / 2014-06-03 ================== * switch from set-type to mime-types 0.6.1 / 2014-05-11 ================== * bump type-is * bump koa-compose 0.6.0 / 2014-05-01 ================== * add nicer error formatting * add: assert object type in ctx.onerror * change .status default to 404. Closes #263 * remove .outputErrors, suppress output when handled by the dev. Closes #272 * fix content-length when body is re-assigned. Closes #267 0.5.5 / 2014-04-14 ================== * fix length when .body is missing * fix: make sure all intermediate stream bodies will be destroyed 0.5.4 / 2014-04-12 ================== * fix header stripping in a few cases 0.5.3 / 2014-04-09 ================== * change res.type= to always default charset. Closes #252 * remove ctx.inspect() implementation. Closes #164 0.5.2 / 2014-03-23 ================== * fix: inspection of `app` and `app.toJSON()` * fix: let `this.throw`n errors provide their own status * fix: overwriting of `content-type` w/ `HEAD` requests * refactor: use statuses * refactor: use escape-html * bump dev deps 0.5.1 / 2014-03-06 ================== * add request.hostname(getter). Closes #224 * remove response.charset and ctx.charset (too confusing in relation to ctx.type) [breaking change] * fix a debug() name 0.5.0 / 2014-02-19 ================== * add context.charset * add context.charset= * add request.charset * add response.charset * add response.charset= * fix response.body= html content sniffing * change ctx.length and ctx.type to always delegate to response object [breaking change] 0.4.0 / 2014-02-11 ================== * remove app.jsonSpaces settings - moved to [koa-json](https://github.com/koajs/json) * add this.response=false to bypass koa's response handling * fix response handling after body has been sent * changed ctx.throw() to no longer .expose 5xx errors * remove app.keys getter/setter, update cookies, and remove keygrip deps * update fresh * update koa-compose 0.3.0 / 2014-01-17 ================== * add ctx.host= delegate * add req.host= * add: context.throw supports Error instances * update co * update cookies 0.2.1 / 2013-12-30 ================== * add better 404 handling * add check for fn._name in debug() output * add explicit .toJSON() calls to ctx.toJSON() 0.2.0 / 2013-12-28 ================== * add support for .throw(status, msg). Closes #130 * add GeneratorFunction assertion for app.use(). Closes #120 * refactor: move `.is()` to `type-is` * refactor: move content negotiation to "accepts" * refactor: allow any streams with .pipe method * remove `next` in callback for now 0.1.2 / 2013-12-21 ================== * update co, koa-compose, keygrip * use on-socket-error * add throw(status, msg) support * assert middleware is GeneratorFunction * ducktype stream checks * remove `next` is `app.callback()` 0.1.1 / 2013-12-19 ================== * fix: cleanup socker error handler on response ================================================ FILE: LICENSE ================================================ (The MIT License) 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 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Readme.md ================================================ Koa middleware framework for nodejs [![gitter][gitter-image]][gitter-url] [![NPM version][npm-image]][npm-url] [![build status][github-action-image]][github-action-url] [![Test coverage][coveralls-image]][coveralls-url] [![OpenCollective Backers][backers-image]](#backers) [![OpenCollective Sponsors][sponsors-image]](#sponsors) [![PR's Welcome][pr-welcoming-image]][pr-welcoming-url] Expressive HTTP middleware framework for node.js to make web applications and APIs more enjoyable to write. Koa's middleware stack flows in a stack-like manner, allowing you to perform actions downstream then filter and manipulate the response upstream. Only methods that are common to nearly all HTTP servers are integrated directly into Koa's small ~570 SLOC codebase. This 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 v18.0.0__ or higher for ES2015 and async function support. ```sh npm install koa ``` ## Hello Koa ```js const Koa = require('koa'); const app = new Koa(); // response 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. - [Guide](docs/guide.md) - Go straight to the docs. ## 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`); }); }); ``` ### Koa v1.x Middleware Signature The middleware signature changed between v1.x and v2.x. The older signature is deprecated. **Old signature middleware support has been removed in v3** Please see the [Migration Guide from v2.x to v3.x](docs/migration-v2-to-v3.md) for information on upgrading from v2.x to v3.x, and the [Migration Guide from v1.x to v2.x](docs/migration-v1-to-v2.md) for information on upgrading from v1.x to v2.x. ## 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. Here is an example of checking that a requesting client supports xml. ```js app.use(async (ctx, next) => { ctx.assert(ctx.request.accepts('xml'), 406); // equivalent to: // if (!ctx.request.accepts('xml')) ctx.throw(406); await next(); }); ``` Koa provides a `Response` object as the `response` property of the `Context`. Koa's `Response` object provides helpful methods for working with http responses which delegate to a [ServerResponse](https://nodejs.org/api/http.html#http_class_http_serverresponse) . 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` and `ctx.accepts` can be used instead of `ctx.request.accepts`. 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 ``` $ 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), [jonathanong](mailto:me@jongleberry.com), and [niftylettuce](mailto:niftylettuce@gmail.com) to disclose. ## Authors See [AUTHORS](AUTHORS). ## Community - [KoaJS Slack Group](https://join.slack.com/t/koa-js/shared_invite/zt-5pjgthmb-1JeKDbByqqcARtlPbtf~vQ) - [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) - [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 ## Backers Support us with a monthly donation and help us continue our activities. ## Sponsors Become a sponsor and get your logo on our README on Github with a link to your site. # License [MIT](https://github.com/koajs/koa/blob/master/LICENSE) [npm-image]: https://img.shields.io/npm/v/koa.svg?style=flat-square [npm-url]: https://www.npmjs.com/package/koa [github-action-image]: https://github.com/koajs/koa/actions/workflows/node.js.yml/badge.svg [github-action-url]: https://github.com/koajs/koa/actions/workflows/node.js.yml [coveralls-image]: https://img.shields.io/codecov/c/github/koajs/koa.svg?style=flat-square [coveralls-url]: https://codecov.io/github/koajs/koa?branch=master [backers-image]: https://opencollective.com/koajs/backers/badge.svg?style=flat-square [sponsors-image]: https://opencollective.com/koajs/sponsors/badge.svg?style=flat-square [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 ================================================ FILE: __tests__/.eslintrc.yml ================================================ env: jest: true rules: space-before-blocks: [2, {functions: never, keywords: always}] no-unused-expressions: 0 node/no-deprecated-api: 'warn' quote-props: 'warn' no-prototype-builtins: 'warn' array-bracket-spacing: 'warn' object-curly-spacing: 'warn' dot-notation: 'warn' ================================================ FILE: __tests__/application/compose.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const request = require('supertest') const assert = require('node:assert/strict') const Koa = require('../..') describe('app.compose', () => { it('should work with default compose ', async () => { const app = new Koa() const calls = [] app.use((ctx, next) => { calls.push(1) return next().then(() => { calls.push(4) }) }) app.use((ctx, next) => { calls.push(2) return next().then(() => { calls.push(3) }) }) await request(app.callback()) .get('/') .expect(404) assert.deepStrictEqual(calls, [1, 2, 3, 4]) }) it('should work with configurable compose', async () => { const calls = [] let count = 0 const app = new Koa({ compose (fns) { return async (ctx) => { const dispatch = async () => { count++ const fn = fns.shift() fn && fn(ctx, dispatch) } dispatch() } } }) app.use((ctx, next) => { calls.push(1) next() calls.push(4) }) app.use((ctx, next) => { calls.push(2) next() calls.push(3) }) await request(app.callback()) .get('/') assert.deepStrictEqual(calls, [1, 2, 3, 4]) assert.equal(count, 3) }) }) ================================================ FILE: __tests__/application/context.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const request = require('supertest') const assert = require('node:assert/strict') 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.strictEqual(ctx.msg, 'hello') ctx.status = 204 }) return request(app1.callback()) .get('/') .expect(204) }) it('should not affect the original prototype', () => { app2.use((ctx, next) => { assert.strictEqual(ctx.msg, undefined) ctx.status = 204 }) return request(app2.callback()) .get('/') .expect(204) }) }) ================================================ FILE: __tests__/application/currentContext.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const request = require('supertest') const assert = require('node:assert/strict') const Koa = require('../..') const { AsyncLocalStorage } = require('async_hooks') describe('app.currentContext', () => { it('should get currentContext return context when asyncLocalStorage enable', async () => { const app = new Koa({ asyncLocalStorage: true }) app.use(async ctx => { assert(ctx === app.currentContext) await new Promise(resolve => { setTimeout(() => { assert(ctx === app.currentContext) resolve() }, 1) }) await new Promise(resolve => { assert(ctx === app.currentContext) setImmediate(() => { assert(ctx === app.currentContext) resolve() }) }) assert(ctx === app.currentContext) app.currentContext.body = 'ok' }) const requestServer = async () => { assert(app.currentContext === undefined) await request(app.callback()).get('/').expect('ok') assert(app.currentContext === undefined) } await Promise.all([ requestServer(), requestServer(), requestServer(), requestServer(), requestServer() ]) }) it('should get currentContext return undefined when asyncLocalStorage disable', async () => { const app = new Koa() app.use(async ctx => { assert(app.currentContext === undefined) ctx.body = 'ok' }) await request(app.callback()).get('/').expect('ok') }) it('should get currentContext return context in error handler when asyncLocalStorage enable', async () => { const app = new Koa({ asyncLocalStorage: true }) app.use(async () => { throw new Error('error message') }) const handleError = new Promise((resolve, reject) => { app.on('error', (err, ctx) => { try { assert.strictEqual(err.message, 'error message') assert.strictEqual(app.currentContext, ctx) resolve() } catch (e) { reject(e) } }) }) await request(app.callback()).get('/').expect('Internal Server Error') await handleError }) it('should get currentContext return undefined in error handler when asyncLocalStorage disable', async () => { const app = new Koa() app.use(async () => { throw new Error('error message') }) const handleError = new Promise((resolve, reject) => { app.on('error', (err, ctx) => { try { assert.strictEqual(err.message, 'error message') assert.strictEqual(app.currentContext, undefined) resolve() } catch (e) { reject(e) } }) }) await request(app.callback()).get('/').expect('Internal Server Error') await handleError }) it('should support a custom asyncLocalStorage', async () => { const asyncLocalStorage = new AsyncLocalStorage() const app = new Koa({ asyncLocalStorage }) assert(app.currentContext === undefined) app.use(async ctx => { assert(ctx === app.currentContext) assert(asyncLocalStorage.getStore() === ctx) ctx.body = 'ok' }) await request(app.callback()).get('/').expect('ok') assert(app.currentContext === undefined) }) }) ================================================ FILE: __tests__/application/index.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const { once } = require('node:events') const Koa = require('../..') describe('app', () => { it('should handle socket errors', async () => { const app = new Koa() let errorCaught = false app.use((ctx) => { ctx.socket.destroy(new Error('boom')) }) app.on('error', err => { assert.strictEqual(err.message, 'boom') errorCaught = true }) const server = app.listen() try { const req = require('http').get({ port: server.address().port }) req.on('error', () => {}) const [err] = await once(app, 'error') assert.strictEqual(err.message, 'boom') assert.strictEqual(errorCaught, true) } finally { await server.close() } }) 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.strictEqual(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 signed cookie keys from the constructor', () => { const keys = ['customkey'] const app = new Koa({ keys }) assert.strictEqual(app.keys, keys) }) it('should set subdomainOffset from the constructor', () => { const subdomainOffset = 3 const app = new Koa({ subdomainOffset }) assert.strictEqual(app.subdomainOffset, subdomainOffset) }) it('should set compose from the constructor', () => { const compose = () => (ctx) => {} const app = new Koa.default({ compose }) // eslint-disable-line new-cap assert.strictEqual(app.compose, compose) }) it('should have a static property exporting `HttpError` from http-errors library', () => { const CreateError = require('http-errors') assert.notEqual(Koa.HttpError, undefined) assert.deepStrictEqual(Koa.HttpError, CreateError.HttpError) assert.throws(() => { throw new CreateError(500, 'test error') }, Koa.HttpError) }) }) ================================================ FILE: __tests__/application/inspect.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const util = require('util') const Koa = require('../..') process.env.NODE_ENV = 'test' const app = new Koa() describe('app.inspect()', () => { it('should work', () => { const str = util.inspect(app) assert.strictEqual("{ subdomainOffset: 2, proxy: false, env: 'test' }", str) }) it('should return a json representation', () => { assert.deepStrictEqual( { subdomainOffset: 2, proxy: false, env: 'test' }, app.inspect() ) }) }) ================================================ FILE: __tests__/application/onerror.test.js ================================================ 'use strict' const { describe, it, mock } = require('node:test') const assert = require('node:assert/strict') const Koa = require('../..') describe('app.onerror(err)', () => { 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 accept errors coming from other scopes', () => { const ExternError = require('vm').runInNewContext('Error') const app = new Koa() const error = Object.assign(new ExternError('boom'), { status: 418, expose: true }) assert.doesNotThrow(() => app.onerror(error)) }) it('should do nothing if status is 404', () => { const app = new Koa() const err = new Error() err.status = 404 const spy = mock.method(console, 'error', () => {}) app.onerror(err) assert.strictEqual(spy.mock.calls.length, 0) spy.mock.restore() }) it('should do nothing if .silent', () => { const app = new Koa() app.silent = true const err = new Error() const spy = mock.method(console, 'error', () => {}) app.onerror(err) assert.strictEqual(spy.mock.calls.length, 0) spy.mock.restore() }) it('should log the error to stderr', () => { const app = new Koa() app.env = 'dev' const err = new Error() err.stack = 'Foo' const spy = mock.method(console, 'error', () => {}) app.onerror(err) assert.notStrictEqual(spy.mock.calls.length, 0) spy.mock.restore() }) }) ================================================ FILE: __tests__/application/request.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const request = require('supertest') const assert = require('node:assert/strict') 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.strictEqual(ctx.request.message, 'hello') ctx.status = 204 }) return request(app1.callback()) .get('/') .expect(204) }) it('should not affect the original prototype', () => { app2.use((ctx, next) => { assert.strictEqual(ctx.request.message, undefined) ctx.status = 204 }) return request(app2.callback()) .get('/') .expect(204) }) }) ================================================ FILE: __tests__/application/respond.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const request = require('supertest') const statuses = require('statuses') const assert = require('node:assert/strict') 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') }) }) return request(app.callback()) .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') }) return request(app.callback()) .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 }) return request(app.callback()) .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 res = await request(app.callback()) .get('/') .expect(200) assert.strictEqual(Object.prototype.hasOwnProperty.call(res.headers, '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 res = await request(app.callback()) .head('/') .expect(200) assert.strictEqual(res.headers['content-type'], 'text/plain; charset=utf-8') assert.strictEqual(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 res = await request(app.callback()) .head('/') .expect(200) assert.strictEqual(res.headers['content-type'], 'application/json; charset=utf-8') assert.strictEqual(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 res = await request(app.callback()) .head('/') .expect(200) assert.strictEqual(res.headers['content-type'], 'text/plain; charset=utf-8') assert.strictEqual(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 res = await request(app.callback()) .head('/') .expect(200) assert.strictEqual(res.headers['content-type'], 'application/octet-stream') assert.strictEqual(res.headers['content-length'], '11') assert(!res.text) }) it('should keep stream header if set manually', async () => { const app = new Koa() const { length } = fs.readFileSync('package.json') app.use(ctx => { ctx.length = length ctx.body = fs.createReadStream('package.json') }) const res = await request(app.callback()) .head('/') .expect(200) assert.strictEqual(~~res.header['content-length'], length) assert(!res.text) }) it('should respond with a 404 if no body was set', () => { const app = new Koa() app.use(ctx => { }) return request(app.callback()) .head('/') .expect(404) }) it('should respond with a 200 if body = ""', () => { const app = new Koa() app.use(ctx => { ctx.body = '' }) return request(app.callback()) .head('/') .expect(200) }) it('should not overwrite the content-type', () => { const app = new Koa() app.use(ctx => { ctx.status = 200 ctx.type = 'application/javascript' }) return request(app.callback()) .head('/') .expect('content-type', /application\/javascript/) .expect(200) }) }) describe('when no middleware is present', () => { it('should 404', () => { const app = new Koa() return request(app.callback()) .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 }) return request(app.callback()) .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) }) }) return request(app.callback()) .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 }) return request(app.callback()) .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 res = await request(app.callback()) .get('/') .expect(204) .expect('') assert.strictEqual(Object.prototype.hasOwnProperty.call(res.headers, '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 res = await request(app.callback()) .get('/') .expect(205) .expect('') assert.strictEqual(Object.prototype.hasOwnProperty.call(res.headers, '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 res = await request(app.callback()) .get('/') .expect(304) .expect('') assert.strictEqual(Object.prototype.hasOwnProperty.call(res.headers, 'Content-Type'), false) }) }) describe('with custom status=700', () => { it('should respond with the associated status message', async () => { const app = new Koa() statuses.message['700'] = 'custom status' app.use(ctx => { ctx.status = 700 }) const res = await request(app.callback()) .get('/') .expect(700) .expect('custom status') assert.strictEqual(res.res.statusMessage, 'custom status') }) }) 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 res = await request(app.callback()) .get('/') .expect(200) .expect('ok') assert.strictEqual(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 }) return request(app.callback()) .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 res = await request(app.callback()) .get('/') .expect(204) .expect('') assert.strictEqual(Object.prototype.hasOwnProperty.call(res.headers, '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 res = await request(app.callback()) .get('/') .expect(204) .expect('') assert.strictEqual(Object.prototype.hasOwnProperty.call(res.headers, '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 res = await request(app.callback()) .get('/') .expect(205) .expect('') assert.strictEqual(Object.prototype.hasOwnProperty.call(res.headers, '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 res = await request(app.callback()) .get('/') .expect(304) .expect('') assert.strictEqual(Object.prototype.hasOwnProperty.call(res.headers, 'Content-Type'), false) }) }) describe('when .body is undefined', () => { it('should respond 204 by default', async () => { const app = new Koa() app.use(ctx => { ctx.body = undefined }) const res = await request(app.callback()) .get('/') .expect(204) .expect('') assert.strictEqual(Object.prototype.hasOwnProperty.call(res.headers, 'Content-Type'), false) }) it('should respond 204 with status=200', async () => { const app = new Koa() app.use(ctx => { ctx.status = 200 ctx.body = undefined }) const res = await request(app.callback()) .get('/') .expect(204) .expect('') assert.strictEqual(Object.prototype.hasOwnProperty.call(res.headers, 'Content-Type'), false) }) }) describe('when .body is a string', () => { it('should respond', () => { const app = new Koa() app.use(ctx => { ctx.body = 'Hello' }) return request(app.callback()) .get('/') .expect('Hello') }) }) describe('when .body is a Buffer', () => { it('should respond', () => { const app = new Koa() app.use(ctx => { ctx.body = Buffer.from('Hello') }) return request(app.callback()) .get('/') .expect(200) .expect(Buffer.from('Hello')) }) }) describe('when .body is a Blob', () => { it('should respond', async () => { const app = new Koa() app.use(ctx => { ctx.body = new Blob(['Hello']) }) const expectedBlob = new Blob(['Hello']) const res = await request(app.callback()) .get('/') .expect(200) assert.deepStrictEqual(res.body, Buffer.from(await expectedBlob.arrayBuffer())) }) it('should keep Blob headers', async () => { const app = new Koa() app.use(ctx => { ctx.body = new Blob(['hello world']) }) return request(app.callback()) .head('/') .expect(200) .expect('content-type', 'application/octet-stream') .expect('content-length', '11') }) }) describe('when .body is a ReadableStream', () => { it('should respond', async () => { const app = new Koa() app.use(async ctx => { ctx.body = new ReadableStream() }) return request(app.callback()) .head('/') .expect(200) .expect('content-type', 'application/octet-stream') }) it('should respond hello', async () => { const app = new Koa() app.use(async ctx => { const blob = new Blob(['hello']) ctx.body = blob.stream() }) return request(app.callback()) .get('/') .expect(200) .expect('content-type', 'application/octet-stream') .expect(Buffer.from('hello')) }) it('should handle ReadableStream with chunks', async () => { const app = new Koa() app.use(async ctx => { const stream = new ReadableStream({ start (controller) { controller.enqueue(new TextEncoder().encode('Hello ')) controller.enqueue(new TextEncoder().encode('World')) controller.close() } }) ctx.body = stream }) return request(app.callback()) .get('/') .expect(200) .expect('content-type', 'application/octet-stream') .expect(Buffer.from('Hello World')) }) it('should handle ReadableStream with custom headers', async () => { const app = new Koa() app.use(async ctx => { ctx.type = 'text/plain' ctx.body = new ReadableStream({ start (controller) { controller.enqueue(new TextEncoder().encode('test content')) controller.close() } }) }) const res = await request(app.callback()) .get('/') .expect(200) .expect('content-type', 'text/plain; charset=utf-8') assert.strictEqual(res.text, 'test content') }) }) describe('when .body is a Response', () => { it('should keep Response headers', () => { const app = new Koa() app.use(ctx => { ctx.body = new Response(null, { status: 201, statusText: 'OK', headers: { 'Content-Type': 'text/plain' } }) }) return request(app.callback()) .head('/') .expect(201) .expect('content-type', 'text/plain') .expect('content-length', '2') }) it('should default to octet-stream', () => { const app = new Koa() app.use(ctx => { ctx.body = new Response(null, { status: 200, statusText: 'OK' }) }) return request(app.callback()) .get('/') .expect(200) .expect('content-type', 'application/octet-stream') .expect(Buffer.from([])) }) it('should respond with body content', async () => { const app = new Koa() app.use(ctx => { ctx.body = new Response('Hello World', { status: 200, headers: { 'Content-Type': 'text/plain' } }) }) const res = await request(app.callback()) .get('/') .expect(200) .expect('content-type', 'text/plain') assert.strictEqual(res.text, 'Hello World') }) it('should handle Response from fetch() with JSON', async () => { const app = new Koa() app.use(async ctx => { const jsonData = JSON.stringify({ message: 'Hello from fetch', timestamp: Date.now() }) const response = new Response(jsonData, { status: 200, headers: { 'Content-Type': 'application/json', 'X-Custom-Header': 'custom-value' } }) ctx.body = response }) const res = await request(app.callback()) .get('/') .expect(200) .expect('content-type', 'application/json') const body = JSON.parse(res.text) assert.strictEqual(body.message, 'Hello from fetch') assert(body.timestamp) }) it('should handle Response from fetch() with streaming body', async () => { const app = new Koa() app.use(async ctx => { const stream = new ReadableStream({ start (controller) { controller.enqueue(new TextEncoder().encode('Streaming ')) controller.enqueue(new TextEncoder().encode('response ')) controller.enqueue(new TextEncoder().encode('from fetch')) controller.close() } }) const response = new Response(stream, { status: 200, headers: { 'Content-Type': 'text/plain' } }) ctx.body = response }) const res = await request(app.callback()) .get('/') .expect(200) .expect('content-type', 'text/plain') assert.strictEqual(res.text, 'Streaming response from fetch') }) it('should handle Response from fetch() with Blob body', async () => { const app = new Koa() app.use(async ctx => { const blob = new Blob(['Hello from Blob'], { type: 'text/plain' }) const response = new Response(blob, { status: 200, headers: { 'Content-Type': 'text/plain' } }) ctx.body = response }) const res = await request(app.callback()) .get('/') .expect(200) .expect('content-type', 'text/plain') assert.strictEqual(res.text, 'Hello from Blob') }) }) 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 res = await request(app.callback()) .get('/') .expect('Content-Type', 'application/json; charset=utf-8') const pkg = require('../../package') assert.strictEqual(Object.prototype.hasOwnProperty.call(res.headers, 'content-length'), false) assert.deepStrictEqual(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 res = await request(app.callback()) .get('/') .expect('Content-Type', 'application/json; charset=utf-8') const pkg = require('../../package') assert.strictEqual(Object.prototype.hasOwnProperty.call(res.headers, 'content-length'), false) assert.deepStrictEqual(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 res = await request(app.callback()) .get('/') .expect('Content-Type', 'application/json; charset=utf-8') const pkg = require('../../package') assert.strictEqual(Object.prototype.hasOwnProperty.call(res.headers, 'content-length'), true) assert.deepStrictEqual(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 res = await request(app.callback()) .get('/') .expect('Content-Type', 'application/json; charset=utf-8') const pkg = require('../../package') assert.strictEqual(Object.prototype.hasOwnProperty.call(res.headers, 'content-length'), true) assert.deepStrictEqual(res.body, pkg) }) 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') }) return request(app.callback()) .get('/') .expect(204) }) }) describe('when using pipeline for streams', () => { it('should handle stream errors when error listener exists', async () => { const app = new Koa() const PassThrough = require('stream').PassThrough let errorCaught = false app.once('error', err => { assert(err.message === 'stream error') errorCaught = true }) app.use(ctx => { const stream = new PassThrough() ctx.body = stream setImmediate(() => { stream.emit('error', new Error('stream error')) }) }) await request(app.callback()) .get('/') .catch(() => {}) await new Promise(resolve => setTimeout(resolve, 50)) assert(errorCaught, 'Error should have been caught') }) it('should not crash when stream errors and no error listener exists', async () => { const app = new Koa() const PassThrough = require('stream').PassThrough app.use(ctx => { const stream = new PassThrough() ctx.body = stream setImmediate(() => { stream.emit('error', new Error('stream error')) }) }) await request(app.callback()) .get('/') .catch(() => {}) await new Promise(resolve => setTimeout(resolve, 50)) }) it('should handle ReadableStream errors when error listener exists', async () => { const app = new Koa() let errorCaught = false app.once('error', err => { assert(err.message === 'readable stream error') errorCaught = true }) app.use(ctx => { const readable = new ReadableStream({ start (controller) { controller.enqueue(new TextEncoder().encode('data')) controller.error(new Error('readable stream error')) } }) ctx.body = readable }) await request(app.callback()) .get('/') .catch(() => {}) await new Promise(resolve => setTimeout(resolve, 50)) assert(errorCaught, 'Error should have been caught') }) it('should cleanup streams on client abort', async () => { const app = new Koa() const PassThrough = require('stream').PassThrough const http = require('http') let streamDestroyed = false app.use(ctx => { const stream = new PassThrough() stream.on('close', () => { streamDestroyed = true }) ctx.body = stream setImmediate(() => { stream.write('some data') }) }) const server = app.listen() await new Promise((resolve) => { const req = http.request({ port: server.address().port, path: '/' }) req.on('response', (res) => { res.on('data', () => { req.destroy() setTimeout(() => { server.close() resolve() }, 50) }) }) req.end() }) assert(streamDestroyed, 'Stream should be destroyed on client abort') }) }) describe('when .body is an Object', () => { it('should respond with json', () => { const app = new Koa() app.use(ctx => { ctx.body = { hello: 'world' } }) return request(app.callback()) .get('/') .expect('Content-Type', 'application/json; charset=utf-8') .expect('{"hello":"world"}') }) describe('and headers sent', () => { it('should respond with json body and headers', () => { const app = new Koa() app.use(ctx => { ctx.length = 17 ctx.type = 'json' ctx.set('foo', 'bar') ctx.res.flushHeaders() ctx.body = { hello: 'world' } }) return request(app.callback()) .get('/') .expect('Content-Type', 'application/json; charset=utf-8') .expect('Content-Length', '17') .expect('foo', 'bar') .expect('{"hello":"world"}') }) }) }) describe('when an error occurs', () => { it('should emit "error" on the app', async () => { const app = new Koa() let errorCaught = false app.on('error', err => { assert.strictEqual(err.message, 'test error') errorCaught = true }) app.use(ctx => { throw new Error('test error') }) await request(app.callback()) .get('/') .expect(500) assert.strictEqual(errorCaught, true) }) 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!') }) return request(app.callback()) .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!') }) return request(app.callback()) .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 }) return request(app.callback()) .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 res = await request(app.callback()) .get('/') .expect(204) assert.strictEqual(Object.prototype.hasOwnProperty.call(res.headers, 'Content-Type'), false) }) }) describe('with explicit null body', () => { it('should preserve given status', async () => { const app = new Koa() app.use(ctx => { ctx.body = null ctx.status = 404 }) return request(app.callback()) .get('/') .expect(404) .expect('') .expect({}) }) it('should respond with correct headers', async () => { const app = new Koa() app.use(ctx => { ctx.body = null ctx.status = 401 }) const res = await request(app.callback()) .get('/') .expect(401) .expect('') .expect({}) assert.equal(Object.prototype.hasOwnProperty.call(res.headers, 'transfer-encoding'), false) assert.equal(Object.prototype.hasOwnProperty.call(res.headers, 'Content-Type'), false) assert.equal(Object.prototype.hasOwnProperty.call(res.headers, 'content-length'), true) }) it('should return content-length equal to 0', async () => { const app = new Koa() app.use(ctx => { ctx.body = null ctx.status = 401 }) const res = await request(app.callback()) .get('/') .expect(401) .expect('') .expect({}) assert.equal(res.headers['content-length'], '0') }) it('should not overwrite the content-length', async () => { const app = new Koa() app.use(ctx => { ctx.body = null ctx.length = 10 ctx.status = 404 }) const res = await request(app.callback()) .get('/') .expect(404) .expect('') .expect({}) assert.equal(res.headers['content-length'], '0') }) }) }) ================================================ FILE: __tests__/application/response.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const request = require('supertest') const assert = require('node:assert/strict') const Koa = require('../..') describe('app.response', () => { const app1 = new Koa() app1.response.msg = 'hello' const app2 = new Koa() const app3 = new Koa() const app4 = new Koa() const app5 = new Koa() const app6 = new Koa() const app7 = new Koa() it('should merge properties', () => { app1.use((ctx, next) => { assert.strictEqual(ctx.response.msg, 'hello') ctx.status = 204 }) return request(app1.callback()) .get('/') .expect(204) }) it('should not affect the original prototype', () => { app2.use((ctx, next) => { assert.strictEqual(ctx.response.msg, undefined) ctx.status = 204 }) return request(app2.callback()) .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.callback()) .get('/') .expect(404) assert.strictEqual(response.text, '404') }) it('should set ._explicitNullBody correctly', async () => { app4.use((ctx, next) => { ctx.body = null assert.strictEqual(ctx.response._explicitNullBody, true) }) return request(app4.callback()) .get('/') .expect(204) }) it('should not set ._explicitNullBody incorrectly', async () => { app5.use((ctx, next) => { ctx.body = undefined assert.strictEqual(ctx.response._explicitNullBody, undefined) ctx.body = '' assert.strictEqual(ctx.response._explicitNullBody, undefined) ctx.body = false assert.strictEqual(ctx.response._explicitNullBody, undefined) }) return request(app5.callback()) .get('/') .expect(204) }) it('should add Content-Length when Transfer-Encoding is not defined', () => { app6.use((ctx, next) => { ctx.body = 'hello world' }) return request(app6.callback()) .get('/') .expect('Content-Length', '11') .expect(200) }) it('should not add Content-Length when Transfer-Encoding is defined', () => { app7.use((ctx, next) => { ctx.set('Transfer-Encoding', 'chunked') ctx.body = 'hello world' assert.strictEqual(ctx.response.get('Content-Length'), undefined) }) return request(app7.callback()) .get('/') .expect('Transfer-Encoding', 'chunked') .expect(200) }) }) ================================================ FILE: __tests__/application/toJSON.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const Koa = require('../..') describe('app.toJSON()', () => { it('should work', () => { const app = new Koa({ env: 'test' }) const obj = app.toJSON() assert.deepStrictEqual({ subdomainOffset: 2, proxy: false, env: 'test' }, obj) }) }) ================================================ FILE: __tests__/application/use.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const request = require('supertest') const assert = require('node:assert/strict') 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) }) }) await request(app.callback()) .get('/') .expect(404) assert.deepStrictEqual(calls, [1, 2, 3, 4, 5, 6]) }) it('should compose mixed middleware', async () => { const app = new Koa() const calls = [] app.use((ctx, next) => { calls.push(1) return next().then(() => { calls.push(6) }) }) app.use(async (ctx, next) => { calls.push(2) await next() calls.push(5) }) app.use((ctx, next) => { calls.push(3) return next().then(() => { calls.push(4) }) }) await request(app.callback()) .get('/') .expect(404) assert.deepStrictEqual(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!/) }) }) }) ================================================ FILE: __tests__/context/assert.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const context = require('../../test-helpers/context') const assert = require('node:assert/strict') describe('ctx.assert(value, status)', () => { it('should throw an error', () => { const ctx = context() let assertionRan = false try { ctx.assert(false, 404, 'custom message') throw new Error('should not reach here') } catch (err) { assertionRan = true assert.strictEqual(err.status, 404) assert.strictEqual(err.message, 'custom message') assert.strictEqual(err.expose, true) } assert(assertionRan) }) }) ================================================ FILE: __tests__/context/cookies.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const request = require('supertest') const Koa = require('../..') describe('ctx.cookies', () => { describe('ctx.cookies.set()', () => { it('should set an unsigned cookie', async () => { const app = new Koa() app.use((ctx, next) => { ctx.cookies.set('name', 'jon') ctx.status = 204 }) const res = await request(app.callback()) .get('/') .expect(204) const cookie = res.headers['set-cookie'].some(cookie => /^name=/.test(cookie)) assert.strictEqual(cookie, true) }) describe('with .signed', () => { describe('when no .keys are set', () => { it('should error', () => { const app = new Koa() app.use((ctx, next) => { try { ctx.cookies.set('foo', 'bar', { signed: true }) } catch (err) { ctx.body = err.message } }) return request(app.callback()) .get('/') .expect('.keys required for signed cookies') }) }) it('should send a signed cookie', async () => { const app = new Koa() app.keys = ['a', 'b'] app.use((ctx, next) => { ctx.cookies.set('name', 'jon', { signed: true }) ctx.status = 204 }) const res = await request(app.callback()) .get('/') .expect(204) const cookies = res.headers['set-cookie'] assert.strictEqual(cookies.some(cookie => /^name=/.test(cookie)), true) assert.strictEqual(cookies.some(cookie => /(,|^)name\.sig=/.test(cookie)), true) }) }) describe('with secure', () => { it('should get secure from request', async () => { const app = new Koa() app.proxy = true app.keys = ['a', 'b'] app.use(ctx => { ctx.cookies.set('name', 'jon', { signed: true }) ctx.status = 204 }) const res = await request(app.callback()) .get('/') .set('x-forwarded-proto', 'https') // mock secure .expect(204) const cookies = res.headers['set-cookie'] assert.strictEqual(cookies.some(cookie => /^name=/.test(cookie)), true) assert.strictEqual(cookies.some(cookie => /(,|^)name\.sig=/.test(cookie)), true) assert.strictEqual(cookies.every(cookie => /secure/.test(cookie)), true) }) }) }) describe('ctx.cookies=', () => { it('should override cookie work', async () => { const app = new Koa() app.use((ctx, next) => { ctx.cookies = { set (key, value) { ctx.set(key, value) } } ctx.cookies.set('name', 'jon') ctx.status = 204 }) await request(app.callback()) .get('/') .expect('name', 'jon') .expect(204) }) }) }) ================================================ FILE: __tests__/context/inspect.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const prototype = require('../../lib/context') const assert = require('node:assert/strict') const util = require('util') const context = require('../../test-helpers/context') describe('ctx.inspect()', () => { it('should return a json representation', () => { const ctx = context() const toJSON = ctx.toJSON(ctx) assert.deepStrictEqual(toJSON, ctx.inspect()) assert.deepStrictEqual(util.inspect(toJSON), util.inspect(ctx)) }) // console.log(require.cache) will call prototype.inspect() it('should not crash when called on the prototype', () => { assert.deepStrictEqual(prototype, prototype.inspect()) assert.deepStrictEqual(util.inspect(prototype.inspect()), util.inspect(prototype)) }) }) ================================================ FILE: __tests__/context/onerror.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const request = require('supertest') const Koa = require('../..') const context = require('../../test-helpers/context') describe('ctx.onerror(err)', () => { it('should respond', () => { const app = new Koa() app.use((ctx, next) => { ctx.body = 'something else' ctx.throw(418, 'boom') }) return request(app.callback()) .get('/') .expect(418) .expect('Content-Type', 'text/plain; charset=utf-8') .expect('Content-Length', '4') }) it('should unset all headers', async () => { const app = new Koa() app.use((ctx, next) => { ctx.set('Vary', 'Accept-Encoding') ctx.set('X-CSRF-Token', 'asdf') ctx.body = 'response' ctx.throw(418, 'boom') }) const res = await request(app.callback()) .get('/') .expect(418) .expect('Content-Type', 'text/plain; charset=utf-8') .expect('Content-Length', '4') assert.strictEqual(Object.prototype.hasOwnProperty.call(res.headers, 'vary'), false) assert.strictEqual(Object.prototype.hasOwnProperty.call(res.headers, '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 res = await request(app.callback()) .get('/') .expect(418) .expect('Content-Type', 'text/plain; charset=utf-8') .expect('X-New-Header', 'Value') assert.strictEqual(Object.prototype.hasOwnProperty.call(res.headers, 'vary'), false) assert.strictEqual(Object.prototype.hasOwnProperty.call(res.headers, 'x-csrf-token'), false) }) it('should ignore error after headerSent', async () => { const app = new Koa() app.on('error', (err, { res }) => { assert.strictEqual(err.message, 'mock error') assert.strictEqual(err.headerSent, true) res.end() }) app.use(async ctx => { ctx.status = 200 ctx.set('X-Foo', 'Bar') ctx.flushHeaders() await Promise.reject(new Error('mock error')) ctx.body = 'response' }) await request(app.callback()) .get('/') .expect('X-Foo', 'Bar') .expect(200) }) it('should set status specified in the error using statusCode', () => { const app = new Koa() app.use((ctx, next) => { ctx.body = 'something else' const err = new Error('Not found') err.statusCode = 404 throw err }) return request(app.callback()) .get('/') .expect(404) .expect('Content-Type', 'text/plain; charset=utf-8') .expect('Not Found') }) describe('when invalid err.statusCode', () => { 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.statusCode = 'notnumber' throw err }) return request(app.callback()) .get('/') .expect(500) .expect('Content-Type', 'text/plain; charset=utf-8') .expect('Internal Server Error') }) }) }) 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 }) return request(app.callback()) .get('/') .expect(500) .expect('Content-Type', 'text/plain; charset=utf-8') .expect('Internal Server Error') }) }) 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 }) return request(app.callback()) .get('/') .expect(500) .expect('Content-Type', 'text/plain; charset=utf-8') .expect('Internal Server Error') }) }) }) describe('when error from another scope thrown', () => { it('should handle it like a normal error', async () => { const ExternError = require('vm').runInNewContext('Error') const app = new Koa() const error = Object.assign(new ExternError('boom'), { status: 418, expose: true }) app.use((ctx, next) => { throw error }) const gotRightErrorPromise = new Promise((resolve, reject) => { app.on('error', receivedError => { try { assert.strictEqual(receivedError, error) resolve() } catch (e) { reject(e) } }) }) await request(app.callback()) .get('/') .expect(418) await gotRightErrorPromise }) }) describe('when non-error thrown', () => { it('should respond with non-error thrown message', () => { const app = new Koa() app.use((ctx, next) => { throw 'string error' // eslint-disable-line no-throw-literal }) return request(app.callback()) .get('/') .expect(500) .expect('Content-Type', 'text/plain; charset=utf-8') .expect('Internal Server Error') }) it('should use res.getHeaderNames() accessor when available', () => { let removed = 0 const ctx = context() ctx.app.emit = () => {} ctx.res = { getHeaderNames: () => ['content-type', 'content-length'], removeHeader: () => removed++, end: () => {}, emit: () => {} } ctx.onerror(new Error('error')) assert.strictEqual(removed, 2) }) it('should stringify error if it is an object', async () => { const app = new Koa() app.on('error', err => { let assertionRan = false assert.strictEqual(err.message, 'non-error thrown: {"key":"value"}') assertionRan = true assert(assertionRan, 'assertion was not executed') }) app.use(async ctx => { throw { key: 'value' } // eslint-disable-line no-throw-literal }) await request(app.callback()) .get('/') .expect(500) .expect('Internal Server Error') }) }) }) ================================================ FILE: __tests__/context/state.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const request = require('supertest') const assert = require('node:assert/strict') const Koa = require('../..') describe('ctx.state', () => { it('should provide a ctx.state namespace', () => { const app = new Koa() app.use(ctx => { assert.deepStrictEqual(ctx.state, {}) }) return request(app.callback()) .get('/') .expect(404) }) }) ================================================ FILE: __tests__/context/throw.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const context = require('../../test-helpers/context') const assert = require('node:assert/strict') describe('ctx.throw(msg)', () => { it('should set .status to 500', () => { const ctx = context() try { ctx.throw('boom') } catch (err) { assert.strictEqual(err.status, 500) assert.strictEqual(err.expose, false) } }) }) 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.strictEqual(err.status, 500) assert.strictEqual(err.message, 'test') assert.strictEqual(err.expose, false) } }) }) 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.strictEqual(err.status, 422) assert.strictEqual(err.message, 'test') assert.strictEqual(err.expose, true) } }) }) describe('ctx.throw(status, msg)', () => { it('should throw an error', () => { const ctx = context() try { ctx.throw(400, 'name required') } catch (err) { assert.strictEqual(err.message, 'name required') assert.strictEqual(400, err.status) assert.strictEqual(true, err.expose) } }) }) describe('ctx.throw(status)', () => { it('should throw an error', () => { const ctx = context() try { ctx.throw(400) } catch (err) { assert.strictEqual(err.message, 'Bad Request') assert.strictEqual(err.status, 400) assert.strictEqual(err.expose, true) } }) describe('when not valid status', () => { it('should not expose', () => { const ctx = context() try { const err = new Error('some error') err.status = -1 ctx.throw(err) } catch (err) { assert.strictEqual(err.message, 'some error') assert.strictEqual(err.expose, false) } }) }) }) describe('ctx.throw(status, msg, props)', () => { it('should mixin props', () => { const ctx = context() try { ctx.throw(400, 'msg', { prop: true }) } catch (err) { assert.strictEqual(err.message, 'msg') assert.strictEqual(err.status, 400) assert.strictEqual(err.expose, true) assert.strictEqual(err.prop, true) } }) describe('when props include status', () => { it('should be ignored', () => { const ctx = context() try { ctx.throw(400, 'msg', { prop: true, status: -1 }) } catch (err) { assert.strictEqual(err.message, 'msg') assert.strictEqual(err.status, 400) assert.strictEqual(err.expose, true) assert.strictEqual(err.prop, true) } }) }) }) describe('ctx.throw(msg, props)', () => { it('should mixin props', () => { const ctx = context() try { ctx.throw('msg', { prop: true }) } catch (err) { assert.strictEqual(err.message, 'msg') assert.strictEqual(err.status, 500) assert.strictEqual(err.expose, false) assert.strictEqual(err.prop, true) } }) }) describe('ctx.throw(status, props)', () => { it('should mixin props', () => { const ctx = context() try { ctx.throw(400, { prop: true }) } catch (err) { assert.strictEqual(err.message, 'Bad Request') assert.strictEqual(err.status, 400) assert.strictEqual(err.expose, true) assert.strictEqual(err.prop, true) } }) }) describe('ctx.throw(err, props)', () => { it('should mixin props', () => { const ctx = context() try { ctx.throw(new Error('test'), { prop: true }) } catch (err) { assert.strictEqual(err.message, 'test') assert.strictEqual(err.status, 500) assert.strictEqual(err.expose, false) assert.strictEqual(err.prop, true) } }) }) ================================================ FILE: __tests__/context/toJSON.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const context = require('../../test-helpers/context') describe('ctx.toJSON()', () => { it('should return a json representation', () => { const ctx = context() ctx.req.method = 'POST' ctx.req.url = '/items' ctx.req.headers['content-type'] = 'text/plain' ctx.status = 200 ctx.body = '

Hey

' const obj = JSON.parse(JSON.stringify(ctx)) const req = obj.request const res = obj.response assert.deepStrictEqual({ method: 'POST', url: '/items', header: { 'content-type': 'text/plain' } }, req) assert.deepStrictEqual({ status: 200, message: 'OK', header: { 'content-type': 'text/html; charset=utf-8', 'content-length': 10 } }, res) }) }) ================================================ FILE: __tests__/lib/search-params.test.js ================================================ const { describe, it } = require('node:test') const sp = require('../../lib/search-params') const assert = require('node:assert/strict') describe('search-params', () => { describe('stringify', () => { it('Should stringify a simple object', () => { assert.deepStrictEqual(sp.stringify({ a: 1, b: 'b' }), 'a=1&b=b') }) it('Should stringify an object with an array', () => { assert.deepStrictEqual(sp.stringify({ a: [1, 2] }), 'a=1&a=2') }) it('Should stringify an object with an array with a single value', () => { assert.deepStrictEqual(sp.stringify({ a: [1] }), 'a=1') }) it('Stringify an object with an array with a single empty value', () => { assert.deepStrictEqual(sp.stringify({ a: [''] }), 'a=') }) it('Should not stringify an object with a nested object', () => { assert.deepStrictEqual(sp.stringify({ a: { b: 1 } }), 'a=') }) }) describe('parse', () => { it('Should parse a simple query string', () => { assert.deepStrictEqual(sp.parse('a=1&b=2'), { a: '1', b: '2' }) }) it('Should parse a query string with same key and multiple values', () => { assert.deepEqual(sp.parse('a=1&a=2'), { a: ['1', '2'] }) }) it('Should parse a query string with an array with a single empty value', () => { assert.deepStrictEqual(sp.parse('a='), { a: '' }) }) }) }) ================================================ FILE: __tests__/load-with-esm.test.js ================================================ const { describe, it } = require('node:test') const assert = require('node:assert/strict') describe('Load with esm', () => { it('should default export koa', async () => { const exported = await import('koa') const required = require('../') assert.strictEqual(exported.default, required) }) it('should match exports own property names', async () => { const exported = new Set(Object.getOwnPropertyNames(await import('koa'))) const required = new Set(Object.getOwnPropertyNames(require('../'))) // Remove constructor properties + default export. for (const k of ['prototype', 'length', 'name']) { required.delete(k) } // Commented out to "fix" CommonJS, ESM, bundling issue. // @see https://github.com/koajs/koa/issues/1513 // exported.delete('default'); assert.strictEqual(exported.size, required.size) assert.strictEqual([...exported].every(property => required.has(property)), true) }) it('CommonJS exports default property', async () => { const required = require('../') assert.strictEqual(Object.prototype.hasOwnProperty.call(required, 'default'), true) }) it('CommonJS exports default property referencing self', async () => { const required = require('../') assert.strictEqual(required.default, required) }) }) ================================================ FILE: __tests__/request/accept.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const Accept = require('accepts') const assert = require('node:assert/strict') const context = require('../../test-helpers/context') describe('ctx.accept', () => { it('should return an Accept instance', () => { const ctx = context() ctx.req.headers.accept = 'application/*;q=0.2, image/jpeg;q=0.8, text/html, text/plain' assert(ctx.accept instanceof Accept) }) }) describe('ctx.accept=', () => { it('should replace the accept object', () => { const ctx = context() ctx.req.headers.accept = 'text/plain' assert.deepStrictEqual(ctx.accepts(), ['text/plain']) const request = context.request() request.req.headers.accept = 'application/*;q=0.2, image/jpeg;q=0.8, text/html, text/plain' ctx.accept = Accept(request.req) assert.deepStrictEqual(ctx.accepts(), ['text/html', 'text/plain', 'image/jpeg', 'application/*']) }) }) ================================================ FILE: __tests__/request/accepts.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const context = require('../../test-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' assert.deepStrictEqual(ctx.accepts(), ['text/html', 'text/plain', 'image/jpeg', 'application/*']) }) }) }) 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' assert.strictEqual(ctx.accepts('image/png', 'image/tiff'), false) }) }) describe('when Accept is not populated', () => { it('should return the first type', () => { const ctx = context() assert.strictEqual(ctx.accepts('text/html', 'text/plain', 'image/jpeg', 'application/*'), 'text/html') }) }) }) describe('when extensions are given', () => { it('should convert to mime types', () => { const ctx = context() ctx.req.headers.accept = 'text/plain, text/html' assert.strictEqual(ctx.accepts('html'), 'html') assert.strictEqual(ctx.accepts('.html'), '.html') assert.strictEqual(ctx.accepts('txt'), 'txt') assert.strictEqual(ctx.accepts('.txt'), '.txt') assert.strictEqual(ctx.accepts('png'), false) }) }) describe('when an array is given', () => { it('should return the first match', () => { const ctx = context() ctx.req.headers.accept = 'text/plain, text/html' assert.strictEqual(ctx.accepts(['png', 'text', 'html']), 'text') assert.strictEqual(ctx.accepts(['png', 'html']), 'html') }) }) describe('when multiple arguments are given', () => { it('should return the first match', () => { const ctx = context() ctx.req.headers.accept = 'text/plain, text/html' assert.strictEqual(ctx.accepts('png', 'text', 'html'), 'text') assert.strictEqual(ctx.accepts('png', 'html'), 'html') }) }) describe('when value present in Accept is an exact match', () => { it('should return the type', () => { const ctx = context() ctx.req.headers.accept = 'text/plain, text/html' assert.strictEqual(ctx.accepts('text/html'), 'text/html') assert.strictEqual(ctx.accepts('text/plain'), 'text/plain') }) }) describe('when value present in Accept is a type match', () => { it('should return the type', () => { const ctx = context() ctx.req.headers.accept = 'application/json, */*' assert.strictEqual(ctx.accepts('text/html'), 'text/html') assert.strictEqual(ctx.accepts('text/plain'), 'text/plain') assert.strictEqual(ctx.accepts('image/png'), 'image/png') }) }) describe('when value present in Accept is a subtype match', () => { it('should return the type', () => { const ctx = context() ctx.req.headers.accept = 'application/json, text/*' assert.strictEqual(ctx.accepts('text/html'), 'text/html') assert.strictEqual(ctx.accepts('text/plain'), 'text/plain') assert.strictEqual(ctx.accepts('image/png'), false) assert.strictEqual(ctx.accepts('png'), false) }) }) }) ================================================ FILE: __tests__/request/acceptsCharsets.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const context = require('../../test-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' assert.deepStrictEqual(ctx.acceptsCharsets(), ['utf-8', 'utf-7', 'iso-8859-1']) }) }) }) 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' assert.strictEqual(ctx.acceptsCharsets('utf-7', 'utf-8'), 'utf-8') }) }) 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' assert.strictEqual(ctx.acceptsCharsets('utf-16'), false) }) }) }) describe('when Accept-Charset is not populated', () => { it('should return the first type', () => { const ctx = context() assert.strictEqual(ctx.acceptsCharsets('utf-7', 'utf-8'), 'utf-7') }) }) }) 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' assert.strictEqual(ctx.acceptsCharsets(['utf-7', 'utf-8']), 'utf-8') }) }) }) ================================================ FILE: __tests__/request/acceptsEncodings.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const context = require('../../test-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' assert.deepStrictEqual(ctx.acceptsEncodings(), ['gzip', 'compress', 'identity']) assert.strictEqual(ctx.acceptsEncodings('gzip', 'compress'), 'gzip') }) }) describe('when Accept-Encoding is not populated', () => { it('should return identity', () => { const ctx = context() assert.deepStrictEqual(ctx.acceptsEncodings(), ['identity']) assert.strictEqual(ctx.acceptsEncodings('gzip', 'deflate', 'identity'), 'identity') }) }) }) describe('with multiple arguments', () => { it('should return the best fit', () => { const ctx = context() ctx.req.headers['accept-encoding'] = 'gzip, compress;q=0.2' assert.strictEqual(ctx.acceptsEncodings('compress', 'gzip'), 'gzip') assert.strictEqual(ctx.acceptsEncodings('gzip', 'compress'), 'gzip') }) }) describe('with an array', () => { it('should return the best fit', () => { const ctx = context() ctx.req.headers['accept-encoding'] = 'gzip, compress;q=0.2' assert.strictEqual(ctx.acceptsEncodings(['compress', 'gzip']), 'gzip') }) }) }) ================================================ FILE: __tests__/request/acceptsLanguages.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const context = require('../../test-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' assert.deepStrictEqual(ctx.acceptsLanguages(), ['es', 'pt', 'en']) }) }) }) 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' assert.strictEqual(ctx.acceptsLanguages('es', 'en'), 'es') }) }) describe('if no types match', () => { it('should return false', () => { const ctx = context() ctx.req.headers['accept-language'] = 'en;q=0.8, es, pt' assert.strictEqual(ctx.acceptsLanguages('fr', 'au'), false) }) }) }) describe('when Accept-Language is not populated', () => { it('should return the first type', () => { const ctx = context() assert.strictEqual(ctx.acceptsLanguages('es', 'en'), 'es') }) }) }) describe('with an array', () => { it('should return the best fit', () => { const ctx = context() ctx.req.headers['accept-language'] = 'en;q=0.8, es, pt' assert.strictEqual(ctx.acceptsLanguages(['es', 'en']), 'es') }) }) }) ================================================ FILE: __tests__/request/charset.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const request = require('../../test-helpers/context').request const assert = require('node:assert/strict') describe('req.charset', () => { describe('with no content-type present', () => { it('should return ""', () => { const req = request() assert(req.charset === '') }) }) describe('with charset present', () => { it('should return ""', () => { const req = request() req.header['content-type'] = 'text/plain' assert(req.charset === '') }) }) describe('with a charset', () => { it('should return the charset', () => { const req = request() req.header['content-type'] = 'text/plain; charset=utf-8' assert.strictEqual(req.charset, 'utf-8') }) it('should return "" if content-type is invalid', () => { const req = request() req.header['content-type'] = 'application/json; application/text; charset=utf-8' assert.strictEqual(req.charset, '') }) }) }) ================================================ FILE: __tests__/request/fresh.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const context = require('../../test-helpers/context') describe('ctx.fresh', () => { describe('the request method is not GET and HEAD', () => { it('should return false', () => { const ctx = context() ctx.req.method = 'POST' assert.strictEqual(ctx.fresh, false) }) }) 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') assert.strictEqual(ctx.fresh, false) }) }) 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') assert.strictEqual(ctx.fresh, true) }) }) describe('and etag does 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') assert.strictEqual(ctx.fresh, false) }) }) }) }) ================================================ FILE: __tests__/request/get.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const context = require('../../test-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' assert.strictEqual(ctx.get('HOST'), 'http://google.com') assert.strictEqual(ctx.get('Host'), 'http://google.com') assert.strictEqual(ctx.get('host'), 'http://google.com') assert.strictEqual(ctx.get('referer'), 'http://google.com') assert.strictEqual(ctx.get('referrer'), 'http://google.com') }) }) ================================================ FILE: __tests__/request/header.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const request = require('../../test-helpers/context').request describe('req.header', () => { it('should return the request header object', () => { const req = request() assert.deepStrictEqual(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.deepStrictEqual(req.header, req.req.headers) }) }) ================================================ FILE: __tests__/request/headers.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const request = require('../../test-helpers/context').request describe('req.headers', () => { it('should return the request header object', () => { const req = request() assert.deepStrictEqual(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.deepStrictEqual(req.headers, req.req.headers) }) }) ================================================ FILE: __tests__/request/host.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const request = require('../../test-helpers/context').request const assert = require('node:assert/strict') describe('req.host', () => { it('should return host with port', () => { const req = request() req.header.host = 'foo.com:3000' assert.strictEqual(req.host, 'foo.com:3000') }) describe('with no host present', () => { it('should return ""', () => { const req = request() assert.strictEqual(req.host, '') }) }) 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.strictEqual(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.strictEqual(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.strictEqual(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' assert.strictEqual(req.host, 'foo.com') }) 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.strictEqual(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' assert.strictEqual(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.strictEqual(req.host, 'proxy.com:8080') }) }) }) describe('with Host header containing @', () => { it('should correctly parse host from userinfo@host format', () => { const req = request() req.header.host = 'evil.com:fake@legitimate.com' assert.strictEqual(req.host, 'legitimate.com') }) it('should correctly parse host from user@host format', () => { const req = request() req.header.host = 'user@example.com' assert.strictEqual(req.host, 'example.com') }) it('should correctly parse host with port from userinfo@host:port format', () => { const req = request() req.header.host = 'user:pass@example.com:8080' assert.strictEqual(req.host, 'example.com:8080') }) it('should correctly parse @ in X-Forwarded-Host when proxy is trusted', () => { const req = request() req.app.proxy = true req.header['x-forwarded-host'] = 'evil.com:fake@legitimate.com' req.header.host = 'foo.com' assert.strictEqual(req.host, 'legitimate.com') }) it('should correctly parse @ in :authority on HTTP/2', () => { const req = request({ httpVersionMajor: 2, httpVersion: '2.0' }) req.header[':authority'] = 'evil.com:fake@legitimate.com' req.header.host = 'foo.com' assert.strictEqual(req.host, 'legitimate.com') }) it('should return empty string for invalid host with @', () => { const req = request() req.header.host = 'user@' assert.strictEqual(req.host, '') }) }) }) ================================================ FILE: __tests__/request/hostname.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const request = require('../../test-helpers/context').request const assert = require('node:assert/strict') describe('req.hostname', () => { it('should return hostname void of port', () => { const req = request() req.header.host = 'foo.com:3000' assert.strictEqual(req.hostname, 'foo.com') }) describe('with no host present', () => { it('should return ""', () => { const req = request() assert.strictEqual(req.hostname, '') }) }) describe('with IPv6 in host', () => { it('should parse localhost void of port', () => { const req = request() req.header.host = '[::1]' assert.strictEqual(req.hostname, '[::1]') }) it('should parse localhost with port 80', () => { const req = request() req.header.host = '[::1]:80' assert.strictEqual(req.hostname, '[::1]') }) it('should parse localhost with non-special schema port', () => { const req = request() req.header.host = '[::1]:1337' assert.strictEqual(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.strictEqual(req.hostname, '[2001:cdba::3257:9652]') }) it('should return empty string when invalid', () => { const req = request() req.header.host = '[invalidIPv6]' assert.strictEqual(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' assert.strictEqual(req.hostname, 'foo.com') }) }) 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' assert.strictEqual(req.hostname, 'bar.com') }) }) }) describe('with Host header containing @', () => { it('should correctly parse hostname from userinfo@host format', () => { const req = request() req.header.host = 'evil.com:fake@legitimate.com' assert.strictEqual(req.hostname, 'legitimate.com') }) it('should correctly parse hostname from user@host format', () => { const req = request() req.header.host = 'user@example.com' assert.strictEqual(req.hostname, 'example.com') }) it('should correctly parse hostname with port from userinfo@host:port format', () => { const req = request() req.header.host = 'user:pass@example.com:8080' assert.strictEqual(req.hostname, 'example.com') }) it('should correctly parse @ in X-Forwarded-Host when proxy is trusted', () => { const req = request() req.app.proxy = true req.header['x-forwarded-host'] = 'evil.com:fake@legitimate.com' req.header.host = 'foo.com' assert.strictEqual(req.hostname, 'legitimate.com') }) }) }) ================================================ FILE: __tests__/request/href.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const Stream = require('stream') const request = require('supertest') const Koa = require('../../') const context = require('../../test-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' }, socket, __proto__: Stream.Readable.prototype } const ctx = context(req) assert.strictEqual(ctx.href, 'http://localhost/users/1?next=/dashboard') // change it also work ctx.url = '/foo/users/1?next=/dashboard' assert.strictEqual(ctx.href, 'http://localhost/users/1?next=/dashboard') }) it('should work with `GET http://example.com/foo`', async () => { const app = new Koa() app.use(ctx => { ctx.body = ctx.href }) const res = await request(app.callback()) .get('/foo') .set('Host', 'example.com') .expect(200) assert.strictEqual(res.text, 'http://example.com/foo') }) }) ================================================ FILE: __tests__/request/idempotent.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const request = require('../../test-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) { const req = request() req.method = method assert.strictEqual(req.idempotent, true) } }) }) describe('when the request method is not idempotent', () => { it('should return false', () => { const req = request() req.method = 'POST' assert.strictEqual(req.idempotent, false) }) }) }) ================================================ FILE: __tests__/request/inspect.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const request = require('../../test-helpers/context').request const assert = require('node:assert/strict') 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(undefined === req.inspect()) assert(util.inspect(req) === 'undefined') }) }) it('should return a json representation', () => { const req = request() req.method = 'GET' req.url = 'example.com' req.header.host = 'example.com' const expected = { method: 'GET', url: 'example.com', header: { host: 'example.com' } } assert.deepStrictEqual(req.inspect(), expected) assert.deepStrictEqual(util.inspect(req), util.inspect(expected)) }) }) ================================================ FILE: __tests__/request/ip.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const Stream = require('stream') const Koa = require('../..') const Request = require('../../test-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' const request = Request(req, undefined, app) assert.strictEqual(request.ip, '127.0.0.1') }) }) 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' const request = Request(req) assert.strictEqual(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.strictEqual(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.strictEqual(request.ip, '127.0.0.2') req.socket.remoteAddress = '127.0.0.1' assert.strictEqual(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.strictEqual(request.ip, '127.0.0.2') request.ip = '127.0.0.1' assert.strictEqual(request.ip, '127.0.0.1') }) }) ================================================ FILE: __tests__/request/ips.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const request = require('../../test-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' assert.deepStrictEqual(req.ips, []) }) }) 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' assert.deepStrictEqual(req.ips, ['127.0.0.1', '127.0.0.2']) }) }) }) describe('when options.proxyIpHeader is present', () => { describe('and proxy is not trusted', () => { it('should be ignored', () => { const req = request() req.app.proxy = false req.app.proxyIpHeader = 'x-client-ip' req.header['x-client-ip'] = '127.0.0.1,127.0.0.2' assert.deepStrictEqual(req.ips, []) }) }) describe('and proxy is trusted', () => { it('should be used', () => { const req = request() req.app.proxy = true req.app.proxyIpHeader = 'x-client-ip' req.header['x-client-ip'] = '127.0.0.1,127.0.0.2' assert.deepStrictEqual(req.ips, ['127.0.0.1', '127.0.0.2']) }) }) }) describe('when options.maxIpsCount is present', () => { describe('and proxy is not trusted', () => { it('should be ignored', () => { const req = request() req.app.proxy = false req.app.maxIpsCount = 1 req.header['x-forwarded-for'] = '127.0.0.1,127.0.0.2' assert.deepStrictEqual(req.ips, []) }) }) describe('and proxy is trusted', () => { it('should be used', () => { const req = request() req.app.proxy = true req.app.maxIpsCount = 1 req.header['x-forwarded-for'] = '127.0.0.1,127.0.0.2' assert.deepStrictEqual(req.ips, ['127.0.0.2']) }) }) }) }) ================================================ FILE: __tests__/request/is.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const context = require('../../test-helpers/context') const assert = require('node:assert/strict') 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' assert.strictEqual(ctx.is('text/*'), 'text/html') }) describe('when no body is given', () => { it('should return null', () => { const ctx = context() assert.strictEqual(ctx.is(), null) assert.strictEqual(ctx.is('image/*'), null) assert.strictEqual(ctx.is('image/*', 'text/*'), null) }) }) describe('when no content type is given', () => { it('should return false', () => { const ctx = context() ctx.header['transfer-encoding'] = 'chunked' assert.strictEqual(ctx.is(), false) assert.strictEqual(ctx.is('image/*'), false) assert.strictEqual(ctx.is('text/*', 'image/*'), false) }) }) describe('give no types', () => { it('should return the mime type', () => { const ctx = context() ctx.header['content-type'] = 'image/png' ctx.header['transfer-encoding'] = 'chunked' assert.strictEqual(ctx.is(), 'image/png') }) }) 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' assert.strictEqual(ctx.is('png'), 'png') assert.strictEqual(ctx.is('.png'), '.png') assert.strictEqual(ctx.is('image/png'), 'image/png') assert.strictEqual(ctx.is('image/*'), 'image/png') assert.strictEqual(ctx.is('*/png'), 'image/png') assert.strictEqual(ctx.is('jpeg'), false) assert.strictEqual(ctx.is('.jpeg'), false) assert.strictEqual(ctx.is('image/jpeg'), false) assert.strictEqual(ctx.is('text/*'), false) assert.strictEqual(ctx.is('*/jpeg'), false) }) }) 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' assert.strictEqual(ctx.is('png'), 'png') assert.strictEqual(ctx.is('.png'), '.png') assert.strictEqual(ctx.is('text/*', 'image/*'), 'image/png') assert.strictEqual(ctx.is('image/*', 'text/*'), 'image/png') assert.strictEqual(ctx.is('image/*', 'image/png'), 'image/png') assert.strictEqual(ctx.is('image/png', 'image/*'), 'image/png') assert.strictEqual(ctx.is(['text/*', 'image/*']), 'image/png') assert.strictEqual(ctx.is(['image/*', 'text/*']), 'image/png') assert.strictEqual(ctx.is(['image/*', 'image/png']), 'image/png') assert.strictEqual(ctx.is(['image/png', 'image/*']), 'image/png') assert.strictEqual(ctx.is('jpeg'), false) assert.strictEqual(ctx.is('.jpeg'), false) assert.strictEqual(ctx.is('text/*', 'application/*'), false) assert.strictEqual(ctx.is('text/html', 'text/plain', 'application/json; charset=utf-8'), false) }) }) 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' assert.strictEqual(ctx.is('urlencoded'), 'urlencoded') assert.strictEqual(ctx.is('json', 'urlencoded'), 'urlencoded') assert.strictEqual(ctx.is('urlencoded', 'json'), 'urlencoded') }) }) }) ================================================ FILE: __tests__/request/length.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const request = require('../../test-helpers/context').request const assert = require('node:assert/strict') describe('ctx.length', () => { it('should return length in content-length', () => { const req = request() req.header['content-length'] = '10' assert.strictEqual(req.length, 10) }) it('should return undefined with no content-length present', () => { const req = request() assert.strictEqual(req.length, undefined) }) }) ================================================ FILE: __tests__/request/origin.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const Stream = require('stream') const context = require('../../test-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', origin: 'http://example.com' }, socket, __proto__: Stream.Readable.prototype } const ctx = context(req) assert.strictEqual(ctx.origin, 'http://example.com') // change it also work ctx.url = '/foo/users/1?next=/dashboard' assert.strictEqual(ctx.origin, 'http://example.com') }) }) ================================================ FILE: __tests__/request/path.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const context = require('../../test-helpers/context') const parseurl = require('parseurl') describe('ctx.path', () => { it('should return the pathname', () => { const ctx = context() ctx.url = '/login?next=/dashboard' assert.strictEqual(ctx.path, '/login') }) }) describe('ctx.path=', () => { it('should set the pathname', () => { const ctx = context() ctx.url = '/login?next=/dashboard' ctx.path = '/logout' assert.strictEqual(ctx.path, '/logout') assert.strictEqual(ctx.url, '/logout?next=/dashboard') }) it('should change .url but not .originalUrl', () => { const ctx = context({ url: '/login' }) ctx.path = '/logout' assert.strictEqual(ctx.url, '/logout') assert.strictEqual(ctx.originalUrl, '/login') assert.strictEqual(ctx.request.originalUrl, '/login') }) it('should not affect parseurl', () => { const ctx = context({ url: '/login?foo=bar' }) ctx.path = '/login' const url = parseurl(ctx.req) assert.strictEqual(url.path, '/login?foo=bar') }) }) ================================================ FILE: __tests__/request/protocol.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const request = require('../../test-helpers/context').request describe('req.protocol', () => { describe('when encrypted', () => { it('should return "https"', () => { const req = request() req.req.socket = { encrypted: true } assert.strictEqual(req.protocol, 'https') }) }) describe('when unencrypted', () => { it('should return "http"', () => { const req = request() req.req.socket = {} assert.strictEqual(req.protocol, 'http') }) }) 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' assert.strictEqual(req.protocol, 'https') }) 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'] = '' assert.strictEqual(req.protocol, 'http') }) }) }) describe('and proxy is not trusted', () => { it('should not be used', () => { const req = request() req.req.socket = {} req.header['x-forwarded-proto'] = 'https, http' assert.strictEqual(req.protocol, 'http') }) }) }) }) ================================================ FILE: __tests__/request/query.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const context = require('../../test-helpers/context') describe('ctx.query', () => { describe('when missing', () => { it('should return an empty object', () => { const ctx = context({ url: '/' }) assert(!Object.keys(ctx.query).length) }) it('should return the same object each time it\'s accessed', () => { const ctx = context({ url: '/' }) ctx.query.a = '2' assert.strictEqual(ctx.query.a, '2') }) }) it('should return a parsed query string', () => { const ctx = context({ url: '/?page=2' }) assert.strictEqual(ctx.query.page, '2') }) }) describe('ctx.query=', () => { it('should stringify and replace the query string and search', () => { const ctx = context({ url: '/store/shoes' }) ctx.query = { page: 2, color: 'blue' } assert.strictEqual(ctx.url, '/store/shoes?page=2&color=blue') assert.strictEqual(ctx.querystring, 'page=2&color=blue') assert.strictEqual(ctx.search, '?page=2&color=blue') }) it('should change .url but not .originalUrl', () => { const ctx = context({ url: '/store/shoes' }) ctx.query = { page: 2 } assert.strictEqual(ctx.url, '/store/shoes?page=2') assert.strictEqual(ctx.originalUrl, '/store/shoes') assert.strictEqual(ctx.request.originalUrl, '/store/shoes') }) }) ================================================ FILE: __tests__/request/querystring.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const context = require('../../test-helpers/context') const parseurl = require('parseurl') describe('ctx.querystring', () => { it('should return the querystring', () => { const ctx = context({ url: '/store/shoes?page=2&color=blue' }) assert.strictEqual(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.strictEqual(ctx.querystring, '') }) }) }) describe('ctx.querystring=', () => { it('should replace the querystring', () => { const ctx = context({ url: '/store/shoes' }) ctx.querystring = 'page=2&color=blue' assert.strictEqual(ctx.url, '/store/shoes?page=2&color=blue') assert.strictEqual(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.strictEqual(ctx.url, '/store/shoes?page=2&color=blue') assert.strictEqual(ctx.search, '?page=2&color=blue') assert.strictEqual(ctx.query.page, '2') assert.strictEqual(ctx.query.color, 'blue') }) it('should change .url but not .originalUrl', () => { const ctx = context({ url: '/store/shoes' }) ctx.querystring = 'page=2&color=blue' assert.strictEqual(ctx.url, '/store/shoes?page=2&color=blue') assert.strictEqual(ctx.originalUrl, '/store/shoes') assert.strictEqual(ctx.request.originalUrl, '/store/shoes') }) it('should not affect parseurl', () => { const ctx = context({ url: '/login?foo=bar' }) ctx.querystring = 'foo=bar' const url = parseurl(ctx.req) assert.strictEqual(url.path, '/login?foo=bar') }) }) ================================================ FILE: __tests__/request/search.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const context = require('../../test-helpers/context') describe('ctx.search=', () => { it('should replace the search', () => { const ctx = context({ url: '/store/shoes' }) ctx.search = '?page=2&color=blue' assert.strictEqual(ctx.url, '/store/shoes?page=2&color=blue') assert.strictEqual(ctx.search, '?page=2&color=blue') }) it('should update ctx.querystring and ctx.query', () => { const ctx = context({ url: '/store/shoes' }) ctx.search = '?page=2&color=blue' assert.strictEqual(ctx.url, '/store/shoes?page=2&color=blue') assert.strictEqual(ctx.querystring, 'page=2&color=blue') assert.strictEqual(ctx.query.page, '2') assert.strictEqual(ctx.query.color, 'blue') }) it('should change .url but not .originalUrl', () => { const ctx = context({ url: '/store/shoes' }) ctx.search = '?page=2&color=blue' assert.strictEqual(ctx.url, '/store/shoes?page=2&color=blue') assert.strictEqual(ctx.originalUrl, '/store/shoes') assert.strictEqual(ctx.request.originalUrl, '/store/shoes') }) describe('when missing', () => { it('should return ""', () => { const ctx = context({ url: '/store/shoes' }) assert.strictEqual(ctx.search, '') }) }) }) ================================================ FILE: __tests__/request/secure.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const request = require('../../test-helpers/context').request describe('req.secure', () => { it('should return true when encrypted', () => { const req = request() req.req.socket = { encrypted: true } assert.strictEqual(req.secure, true) }) }) ================================================ FILE: __tests__/request/stale.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const context = require('../../test-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"') assert.strictEqual(ctx.fresh, true) assert.strictEqual(ctx.stale, false) }) }) ================================================ FILE: __tests__/request/subdomains.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const request = require('../../test-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 assert.deepStrictEqual(req.subdomains, ['ferrets', 'tobi']) req.app.subdomainOffset = 3 assert.deepStrictEqual(req.subdomains, ['tobi']) }) it('should work with no host present', () => { const req = request() assert.deepStrictEqual(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.deepStrictEqual(req.subdomains, []) }) }) ================================================ FILE: __tests__/request/type.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const request = require('../../test-helpers/context').request const assert = require('node:assert/strict') describe('req.type', () => { it('should return type void of parameters', () => { const req = request() req.header['content-type'] = 'text/html; charset=utf-8' assert.strictEqual(req.type, 'text/html') }) it('should return empty string with no host present', () => { const req = request() assert.strictEqual(req.type, '') }) }) ================================================ FILE: __tests__/request/whatwg-url.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const request = require('../../test-helpers/context').request const assert = require('node:assert/strict') describe('req.URL', () => { it('should not throw when host is void', () => { // Accessing the URL should not throw. request().URL // eslint-disable-line no-unused-expressions }) it('should not throw when header.host is invalid', () => { const req = request() req.header.host = 'invalid host' // Accessing the URL should not throw. req.URL // eslint-disable-line no-unused-expressions }) it('should return empty object when invalid', () => { const req = request() req.header.host = 'invalid host' assert.deepStrictEqual(req.URL, Object.create(null)) }) }) ================================================ FILE: __tests__/response/append.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const context = require('../../test-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') assert.deepStrictEqual(ctx.response.header['x-foo'], ['bar1', 'bar2']) }) it('should accept array of values', () => { const ctx = context() ctx.append('Set-Cookie', ['foo=bar', 'fizz=buzz']) ctx.append('Set-Cookie', 'hi=again') assert.deepStrictEqual(ctx.response.header['set-cookie'], ['foo=bar', 'fizz=buzz', 'hi=again']) }) it('should get reset by res.set(field, val)', () => { const ctx = context() ctx.append('Link', '') ctx.append('Link', '') ctx.set('Link', '') assert.strictEqual(ctx.response.header.link, '') }) it('should work with res.set(field, val) first', () => { const ctx = context() ctx.set('Link', '') ctx.append('Link', '') assert.deepStrictEqual(ctx.response.header.link, ['', '']) }) }) ================================================ FILE: __tests__/response/attachment.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const context = require('../../test-helpers/context') const request = require('supertest') const Koa = require('../..') describe('ctx.attachment([filename])', () => { describe('security: prevent Content-Type override (GHSA-c5vw-j4hf-j526)', () => { it('should NOT override Content-Type when already set', () => { const ctx = context() ctx.response.set('Content-Type', 'application/octet-stream') ctx.attachment('malicious.html') assert.strictEqual(ctx.response.get('Content-Type'), 'application/octet-stream') assert.strictEqual(ctx.response.header['content-disposition'], 'attachment; filename="malicious.html"') }) it('should preserve safe Content-Type for SVG files', () => { const ctx = context() ctx.response.set('Content-Type', 'application/octet-stream') ctx.attachment('image.svg') assert.strictEqual(ctx.response.get('Content-Type'), 'application/octet-stream') }) it('should set Content-Type when not previously set', () => { const ctx = context() ctx.attachment('document.pdf') assert.strictEqual(ctx.response.get('Content-Type'), 'application/pdf') }) }) describe('when given a filename', () => { it('should set the filename param', () => { const ctx = context() ctx.attachment('path/to/tobi.png') const str = 'attachment; filename="tobi.png"' assert.strictEqual(ctx.response.header['content-disposition'], str) }) }) describe('when omitting filename', () => { it('should not set filename param', () => { const ctx = context() ctx.attachment() assert.strictEqual(ctx.response.header['content-disposition'], 'attachment') }) }) describe('when given a non-ascii filename', () => { it('should set the encodeURI filename param', () => { const ctx = context() ctx.attachment('path/to/include-no-ascii-char-中文名-ok.png') 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.strictEqual(ctx.response.header['content-disposition'], str) }) it('should work with http client', () => { const app = new Koa() app.use((ctx, next) => { ctx.attachment('path/to/include-no-ascii-char-中文名-ok.json') ctx.body = { foo: 'bar' } }) 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.strictEqual(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.strictEqual(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.strictEqual(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.strictEqual(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.strictEqual(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.strictEqual(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.strictEqual(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.strictEqual(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.strictEqual(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.strictEqual(ctx.response.header['content-disposition'], 'attachment') }) }) }) describe('with "type" option', () => { it('should default to attachment', () => { const ctx = context() ctx.attachment() assert.strictEqual(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.strictEqual(ctx.response.header['content-disposition'], 'inline') }) it('should create a header with inline type and filename', () => { const ctx = context() ctx.attachment('plans.pdf', { type: 'inline' }) assert.strictEqual(ctx.response.header['content-disposition'], 'inline; filename="plans.pdf"') }) it('should normalize type', () => { const ctx = context() ctx.attachment(undefined, { type: 'INLINE' }) assert.strictEqual(ctx.response.header['content-disposition'], 'inline') }) }) }) ================================================ FILE: __tests__/response/back.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const context = require('../../test-helpers/context') describe('ctx.back([alt])', () => { it('should redirect to Referrer', () => { const ctx = context({ url: '/', headers: { host: 'example.com' } }) ctx.req.headers.referrer = '/login' ctx.back() assert.equal(ctx.response.header.location, '/login') }) it('should redirect to the same origin referrer', () => { const ctx = context() ctx.req.headers.host = 'example.com' ctx.req.headers.referrer = 'https://example.com/login' ctx.back() assert.equal(ctx.response.header.location, 'https://example.com/login') }) it('should redirect to root if the same origin referrer is not present', () => { const ctx = context() ctx.req.headers.host = 'example.com' ctx.req.headers.referrer = 'https://other.com/login' ctx.back() assert.equal(ctx.response.header.location, '/') }) it('should redirect to Referer to a relative path', () => { const ctx = context({ url: '/', headers: { host: 'example.com' } }) ctx.req.headers.referer = '/login' ctx.back() assert.equal(ctx.response.header.location, '/login') }) it('should redirect to Referer to a same origin url', () => { const ctx = context({ url: '/', headers: { host: 'example.com', referer: 'https://example.com/login' } }) ctx.back() assert.equal(ctx.response.header.location, 'https://example.com/login') }) it('should default to alt', () => { const ctx = context() ctx.back('/index.html') assert.equal(ctx.response.header.location, '/index.html') }) it('should default redirect to /', () => { const ctx = context() ctx.back() assert.equal(ctx.response.header.location, '/') }) it('should fix Trailing Double-Slash security issue', () => { const ctx = context({ url: '/', headers: { host: 'example.com' } }) ctx.req.headers.referrer = '//evil.com/login/' ctx.back() assert.equal(ctx.response.header.location, '/') ctx.back('/home') assert.equal(ctx.response.header.location, '/home') }) }) ================================================ FILE: __tests__/response/body.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const response = require('../../test-helpers/context').response const CustomStream = require('../../test-helpers/stream') const assert = require('node:assert/strict') const fs = require('fs') const Stream = require('stream') describe('res.body=', () => { describe('when Content-Type is set', () => { it('should not override', () => { const res = response() res.type = 'png' res.body = Buffer.from('something') assert.strictEqual('image/png', res.header['content-type']) }) describe('when body is an object', () => { it('should override as json', () => { const res = response() res.body = 'hey' assert.strictEqual('text/html; charset=utf-8', res.header['content-type']) res.body = { foo: 'bar' } assert.strictEqual('application/json; charset=utf-8', res.header['content-type']) }) }) it('should override length', () => { const res = response() res.type = 'html' res.body = 'something' assert.strictEqual(res.length, 9) }) }) describe('when a string is given', () => { it('should default to text', () => { const res = response() res.body = 'Tobi' assert.strictEqual('text/plain; charset=utf-8', res.header['content-type']) }) it('should set length', () => { const res = response() res.body = 'Tobi' assert.strictEqual(4, res.header['content-length']) }) describe('and contains a non-leading <', () => { it('should default to text', () => { const res = response() res.body = 'aklsdjf < klajsdlfjasd' assert.strictEqual('text/plain; charset=utf-8', res.header['content-type']) }) }) }) describe('when an html string is given', () => { it('should default to html', () => { const res = response() res.body = '

Tobi

' assert.strictEqual('text/html; charset=utf-8', res.header['content-type']) }) it('should set length', () => { const string = '

Tobi

' const res = response() res.body = string assert.strictEqual(res.length, Buffer.byteLength(string)) }) it('should set length when body is overridden', () => { const string = '

Tobi

' const res = response() res.body = string res.body = string + string assert.strictEqual(res.length, 2 * Buffer.byteLength(string)) }) describe('when it contains leading whitespace', () => { it('should default to html', () => { const res = response() res.body = '

Tobi

' assert.strictEqual('text/html; charset=utf-8', res.header['content-type']) }) }) }) describe('when an xml string is given', () => { it('should default to html', () => { /** * 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 ctx simple test fails. */ const res = response() res.body = '\n<俄语>данные' assert.strictEqual('text/html; charset=utf-8', res.header['content-type']) }) }) describe('when a stream is given', () => { it('should default to an octet stream', () => { const res = response() res.body = fs.createReadStream('LICENSE') assert.strictEqual('application/octet-stream', res.header['content-type']) }) it('should support custom stream', () => { const res = response() res.body = new CustomStream.Readable() assert.strictEqual('application/octet-stream', res.header['content-type']) }) it('should not add error handler to stream (handled by pipeline)', () => { const res = response() const body = new Stream.PassThrough() assert.strictEqual(body.listenerCount('error'), 0) res.body = body assert.strictEqual(body.listenerCount('error'), 0) res.body = body assert.strictEqual(body.listenerCount('error'), 0) }) it('should NOT cleanup original stream when replaced by new stream (to support wrapping middleware)', () => { const res = response() const stream1 = new Stream.PassThrough() const stream2 = new Stream.PassThrough() res.body = stream1 res.body = stream2 assert.strictEqual(stream1.destroyed, false) assert.strictEqual(stream2.destroyed, false) }) it('should cleanup original stream when replaced by null', () => { const res = response() const stream = new Stream.PassThrough() res.body = stream res.body = null assert.strictEqual(stream.destroyed, true) }) it('should not throw unhandled errors when replacing failing stream', async () => { const res = response() const stream1 = new Stream.Readable({ read () { } }) const stream2 = new Stream.PassThrough() res.body = stream1 res.body = stream2 await new Promise((resolve) => { process.nextTick(() => { stream1.emit('error', new Error('stream1 error')) setTimeout(resolve, 10) }) }) }) it('should handle multiple sequential stream replacements', () => { const res = response() const stream1 = new Stream.PassThrough() const stream2 = new Stream.PassThrough() const stream3 = new Stream.PassThrough() res.body = stream1 res.body = stream2 res.body = stream3 assert.strictEqual(stream1.destroyed, false) assert.strictEqual(stream2.destroyed, false) assert.strictEqual(stream3.destroyed, false) }) it('should handle four sequential stream replacements', () => { const res = response() const stream1 = new Stream.PassThrough() const stream2 = new Stream.PassThrough() const stream3 = new Stream.PassThrough() const stream4 = new Stream.PassThrough() res.body = stream1 res.body = stream2 res.body = stream3 res.body = stream4 assert.strictEqual(stream1.destroyed, false) assert.strictEqual(stream2.destroyed, false) assert.strictEqual(stream3.destroyed, false) assert.strictEqual(stream4.destroyed, false) }) it('should cleanup stream when replaced by string', () => { const res = response() const stream = new Stream.PassThrough() res.body = stream res.body = 'hello' assert.strictEqual(stream.destroyed, true) }) it('should cleanup stream when replaced by buffer', () => { const res = response() const stream = new Stream.PassThrough() res.body = stream res.body = Buffer.from('hello') assert.strictEqual(stream.destroyed, true) }) it('should cleanup stream when replaced by object', () => { const res = response() const stream = new Stream.PassThrough() res.body = stream res.body = { foo: 'bar' } assert.strictEqual(stream.destroyed, true) }) it('should support wrapping stream middleware (like koa-compress)', async () => { const res = response() const sourceStream = new Stream.Readable({ read () { this.push('hello world') this.push(null) } }) res.body = sourceStream const wrappedStream = new Stream.PassThrough() sourceStream.pipe(wrappedStream) res.body = wrappedStream assert.strictEqual(sourceStream.destroyed, false) assert.strictEqual(wrappedStream.destroyed, false) const chunks = [] for await (const chunk of wrappedStream) { chunks.push(chunk) } const result = Buffer.concat(chunks).toString() assert.strictEqual(result, 'hello world') }) }) describe('when a buffer is given', () => { it('should default to an octet stream', () => { const res = response() res.body = Buffer.from('hey') assert.strictEqual('application/octet-stream', res.header['content-type']) }) it('should set length', () => { const res = response() res.body = Buffer.from('Tobi') assert.strictEqual(4, res.header['content-length']) }) }) describe('when an object is given', () => { it('should default to json', () => { const res = response() res.body = { foo: 'bar' } assert.strictEqual('application/json; charset=utf-8', res.header['content-type']) }) }) describe('when a ReadableStream is given', () => { it('should default to an octet stream', () => { const res = response() res.body = new ReadableStream() assert.strictEqual('application/octet-stream', res.header['content-type']) }) }) describe('when a Blob is given', () => { it('should default to an octet stream', () => { const res = response() res.body = new Blob([new Uint8Array([1, 2, 3])], { type: 'application/octet-stream' }) assert.strictEqual('application/octet-stream', res.header['content-type']) }) it('should set length', () => { const res = response() res.body = new Blob([new Uint8Array([1, 2, 3])], { type: 'application/octet-stream' }) assert.strictEqual(3, res.header['content-length']) }) }) describe('when a response is given', () => { it('should set the status', () => { const res = response() res.body = new Response(null, { status: 201 }) assert.strictEqual(201, res.status) }) it('should set headers', () => { const res = response() res.body = new Response(null, { status: 200, headers: { 'x-fizz': 'buzz', 'x-foo': 'bar' } }) assert.strictEqual('buzz', res.header['x-fizz']) assert.strictEqual('bar', res.header['x-foo']) }) it('should redirect', () => { const res = response() res.body = Response.redirect('https://www.example.com/', 301) assert.strictEqual(301, res.status) assert.strictEqual('https://www.example.com/', res.header.location) }) }) }) ================================================ FILE: __tests__/response/etag.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const response = require('../../test-helpers/context').response describe('res.etag=', () => { it('should not modify an etag with quotes', () => { const res = response() res.etag = '"asdf"' assert.strictEqual(res.header.etag, '"asdf"') }) it('should not modify a weak etag', () => { const res = response() res.etag = 'W/"asdf"' assert.strictEqual(res.header.etag, 'W/"asdf"') }) it('should add quotes around an etag if necessary', () => { const res = response() res.etag = 'asdf' assert.strictEqual(res.header.etag, '"asdf"') }) }) describe('res.etag', () => { it('should return etag', () => { const res = response() res.etag = '"asdf"' assert.strictEqual(res.etag, '"asdf"') }) }) ================================================ FILE: __tests__/response/flushHeaders.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const request = require('supertest') const assert = require('node:assert/strict') const Koa = require('../..') const http = require('http') const { once } = require('node:events') describe('ctx.flushHeaders()', () => { it('should set headersSent', () => { const app = new Koa() app.use((ctx, next) => { ctx.body = 'Body' ctx.status = 200 ctx.flushHeaders() assert.strictEqual(ctx.headerSent, true) assert.strictEqual(ctx.res.headersSent, true) }) return request(app.callback()) .get('/') .expect(200) .expect('Body') }) it('should allow a response afterwards', () => { const app = new Koa() app.use((ctx, next) => { ctx.status = 200 ctx.res.setHeader('Content-Type', 'text/plain') ctx.flushHeaders() ctx.body = 'Body' }) return request(app.callback()) .get('/') .expect(200) .expect('Content-Type', 'text/plain') .expect('Body') }) it('should send the correct status code', () => { const app = new Koa() app.use((ctx, next) => { ctx.status = 401 ctx.res.setHeader('Content-Type', 'text/plain') ctx.flushHeaders() ctx.body = 'Body' }) return request(app.callback()) .get('/') .expect(401) .expect('Content-Type', 'text/plain') .expect('Body') }) it('should ignore set header after flushHeaders', async () => { const app = new Koa() app.use((ctx, next) => { ctx.status = 401 ctx.res.setHeader('Content-Type', 'text/plain') ctx.flushHeaders() ctx.body = 'foo' ctx.set('X-Shouldnt-Work', 'Value') ctx.remove('Content-Type') ctx.vary('Content-Type') }) const res = await request(app.callback()) .get('/') .expect(401) .expect('Content-Type', 'text/plain') assert.strictEqual(res.headers['x-shouldnt-work'], undefined, 'header set after flushHeaders') assert.strictEqual(res.headers.vary, undefined, 'header set after flushHeaders') }) it('should flush headers first and delay to send data', async () => { const PassThrough = require('stream').PassThrough const app = new Koa() let headersFlushed = false let dataReceived = false app.use(ctx => { ctx.type = 'json' ctx.status = 200 ctx.headers.Link = '; as=style; rel=preload, ; rel=preconnect; crossorigin' const stream = ctx.body = new PassThrough() ctx.flushHeaders() headersFlushed = true setTimeout(() => { stream.end(JSON.stringify({ message: 'hello!' })) }, 10) }) const server = app.listen() await once(server, 'listening') const port = server.address().port try { const req = http.request({ port }) req.end() const [res] = await once(req, 'response') assert(headersFlushed, 'Headers should be flushed') const dataPromise = new Promise(resolve => { res.once('data', chunk => { dataReceived = true resolve(chunk) }) }) // Wait for data with a timeout const timeoutPromise = new Promise((resolve, reject) => setTimeout(() => reject(new Error('Timeout waiting for data')), 100) ) await Promise.race([dataPromise, timeoutPromise]) res.destroy() assert(dataReceived, 'Data should be received after headers') } finally { server.close() } }) it('should catch stream error', async () => { const PassThrough = require('stream').PassThrough const app = new Koa() app.once('error', err => { assert(err.message === 'mock error') }) app.use(ctx => { ctx.type = 'json' ctx.status = 200 ctx.headers.Link = '; as=style; rel=preload, ; rel=preconnect; crossorigin' ctx.length = 20 ctx.flushHeaders() const stream = ctx.body = new PassThrough() setTimeout(() => { stream.emit('error', new Error('mock error')) stream.end() }) }) await request(app.callback()).get('/') .then(() => { throw new Error('should not successfully end') }) .catch(err => { assert(err.message === 'aborted') }) }) }) ================================================ FILE: __tests__/response/get.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const context = require('../../test-helpers/context') describe('ctx.get(name)', () => { it('should get a field value, case insensitive', () => { const ctx = context() ctx.set('X-Foo', 'bar') assert.strictEqual(ctx.response.get('x-FOO'), 'bar') }) it('should have the same behavior as ctx.res.getHeader on undefined and null values', () => { const ctx = context() ctx.res.setHeader('X-Foo', undefined) ctx.response.header['x-boo'] = null assert.strictEqual(ctx.response.get('x-FOO'), ctx.res.getHeader('X-FOO')) assert.strictEqual(ctx.response.get('x-bOO'), ctx.res.getHeader('X-BOO')) }) it('should not convert header value type', () => { const ctx = context() ctx.res.setHeader('Foo-date', new Date()) ctx.response.header['foo-map'] = new Map() ctx.res.setHeader('Foo-empty-string', '') ctx.res.setHeader('Foo-number', 0) ctx.res.setHeader('Foo-null', null) ctx.res.setHeader('Foo-undefined', undefined) assert.ok(ctx.response.get('foo-Date') instanceof Date) assert.ok(ctx.response.get('foo-Map') instanceof Map) assert.strictEqual(ctx.response.get('Foo-empty-String'), '') assert.strictEqual(ctx.response.get('Foo-Number'), 0) assert.ok(ctx.response.get('foo-NULL') === null) assert.ok(typeof ctx.response.get('FOO-undefined') === 'undefined') }) it('should return undefined for non-existent headers', () => { const ctx = context() assert.strictEqual(ctx.response.get('nonexistent'), undefined) assert.strictEqual(ctx.response.get(''), undefined) }) }) ================================================ FILE: __tests__/response/has.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const context = require('../../test-helpers/context') describe('ctx.response.has(name)', () => { it('should check a field value, case insensitive way', () => { const ctx = context() ctx.set('X-Foo', '') assert.ok(ctx.response.has('x-Foo')) assert.ok(ctx.has('x-foo')) }) it('should return false for non-existent header', () => { const ctx = context() assert.strictEqual(ctx.response.has('boo'), false) ctx.set('x-foo', 5) assert.strictEqual(ctx.has('x-boo'), false) }) }) ================================================ FILE: __tests__/response/header.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const request = require('supertest') const response = require('../../test-helpers/context').response const Koa = require('../..') describe('res.header', () => { it('should return the response header object', () => { const res = response() res.set('X-Foo', 'bar') res.set('X-Number', 200) assert.deepStrictEqual(res.header, { 'x-foo': 'bar', 'x-number': 200 }) }) it('should use res.getHeaders() accessor when available', () => { const res = response() res.res._headers = null res.res.getHeaders = () => ({ 'x-foo': 'baz' }) assert.deepStrictEqual(res.header, { 'x-foo': 'baz' }) }) it('should return the response header object when no mocks are in use', async () => { const app = new Koa() let header app.use(ctx => { ctx.set('x-foo', '42') header = Object.assign({}, ctx.response.header) }) await request(app.callback()) .get('/') assert.deepStrictEqual(header, { 'x-foo': '42' }) }) describe('when res._headers not present', () => { it('should return empty object', () => { const res = response() res.res._headers = null assert.deepStrictEqual(res.header, {}) }) }) }) ================================================ FILE: __tests__/response/headers.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const response = require('../../test-helpers/context').response describe('res.header', () => { it('should return the response header object', () => { const res = response() res.set('X-Foo', 'bar') assert.deepStrictEqual(res.headers, { 'x-foo': 'bar' }) }) describe('when res._headers not present', () => { it('should return empty object', () => { const res = response() res.res._headers = null assert.deepStrictEqual(res.headers, {}) }) }) }) ================================================ FILE: __tests__/response/inspect.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const response = require('../../test-helpers/context').response const assert = require('node:assert/strict') const util = require('util') describe('res.inspect()', () => { describe('with no response.res present', () => { it('should return null', () => { const res = response() res.body = 'hello' delete res.res assert.strictEqual(res.inspect(), undefined) assert.strictEqual(util.inspect(res), 'undefined') }) }) it('should return a json representation', () => { const res = response() res.body = 'hello' const expected = { status: 200, message: 'OK', header: { 'content-type': 'text/plain; charset=utf-8', 'content-length': 5 }, body: 'hello' } assert.deepStrictEqual(res.inspect(), expected) assert.deepStrictEqual(util.inspect(res), util.inspect(expected)) }) }) ================================================ FILE: __tests__/response/is.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const context = require('../../test-helpers/context') const assert = require('node:assert/strict') describe('response.is(type)', () => { it('should ignore params', () => { const res = context().response res.type = 'text/html; charset=utf-8' assert.strictEqual(res.is('text/*'), 'text/html') }) describe('when no type is set', () => { it('should return false', () => { const res = context().response assert.strictEqual(res.is(), false) assert.strictEqual(res.is('html'), false) }) }) describe('when given no types', () => { it('should return the type', () => { const res = context().response res.type = 'text/html; charset=utf-8' assert.strictEqual(res.is(), 'text/html') }) }) describe('given one type', () => { it('should return the type or false', () => { const res = context().response res.type = 'image/png' assert.strictEqual(res.is('png'), 'png') assert.strictEqual(res.is('.png'), '.png') assert.strictEqual(res.is('image/png'), 'image/png') assert.strictEqual(res.is('image/*'), 'image/png') assert.strictEqual(res.is('*/png'), 'image/png') assert.strictEqual(res.is('jpeg'), false) assert.strictEqual(res.is('.jpeg'), false) assert.strictEqual(res.is('image/jpeg'), false) assert.strictEqual(res.is('text/*'), false) assert.strictEqual(res.is('*/jpeg'), false) }) }) describe('given multiple types', () => { it('should return the first match or false', () => { const res = context().response res.type = 'image/png' assert.strictEqual(res.is('png'), 'png') assert.strictEqual(res.is('.png'), '.png') assert.strictEqual(res.is('text/*', 'image/*'), 'image/png') assert.strictEqual(res.is('image/*', 'text/*'), 'image/png') assert.strictEqual(res.is('image/*', 'image/png'), 'image/png') assert.strictEqual(res.is('image/png', 'image/*'), 'image/png') assert.strictEqual(res.is(['text/*', 'image/*']), 'image/png') assert.strictEqual(res.is(['image/*', 'text/*']), 'image/png') assert.strictEqual(res.is(['image/*', 'image/png']), 'image/png') assert.strictEqual(res.is(['image/png', 'image/*']), 'image/png') assert.strictEqual(res.is('jpeg'), false) assert.strictEqual(res.is('.jpeg'), false) assert.strictEqual(res.is('text/*', 'application/*'), false) assert.strictEqual(res.is('text/html', 'text/plain', 'application/json; charset=utf-8'), false) }) }) describe('when Content-Type: application/x-www-form-urlencoded', () => { it('should match "urlencoded"', () => { const res = context().response res.type = 'application/x-www-form-urlencoded' assert.strictEqual(res.is('urlencoded'), 'urlencoded') assert.strictEqual(res.is('json', 'urlencoded'), 'urlencoded') assert.strictEqual(res.is('urlencoded', 'json'), 'urlencoded') }) }) }) ================================================ FILE: __tests__/response/last-modified.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const response = require('../../test-helpers/context').response describe('res.lastModified', () => { it('should set the header as a UTCString', () => { const res = response() const date = new Date() res.lastModified = date assert.strictEqual(res.header['last-modified'], date.toUTCString()) }) it('should work with date strings', () => { const res = response() const date = new Date() res.lastModified = date.toString() assert.strictEqual(res.header['last-modified'], date.toUTCString()) }) it('should get the header as a Date', () => { // Note: Date() removes milliseconds, but it's practically important. const res = response() const date = new Date() res.lastModified = date assert.strictEqual((res.lastModified.getTime() / 1000), Math.floor(date.getTime() / 1000)) }) describe('when lastModified not set', () => { it('should get undefined', () => { const res = response() assert.strictEqual(res.lastModified, undefined) }) }) }) ================================================ FILE: __tests__/response/length.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const response = require('../../test-helpers/context').response const assert = require('node:assert/strict') const fs = require('fs') describe('res.length', () => { describe('when Content-Length is defined', () => { it('should return a number', () => { const res = response() res.set('Content-Length', '1024') assert.strictEqual(res.length, 1024) }) describe('but not number', () => { it('should return 0', () => { const res = response() res.set('Content-Length', 'hey') assert.strictEqual(res.length, 0) }) }) }) describe('when Content-Length is not defined', () => { describe('and a .body is set', () => { it('should return a number', () => { const res = response() res.body = null assert.strictEqual(res.length, undefined) res.body = 'foo' res.remove('Content-Length') assert.strictEqual(res.length, 3) res.body = 'foo' assert.strictEqual(res.length, 3) res.body = Buffer.from('foo bar') res.remove('Content-Length') assert.strictEqual(res.length, 7) res.body = Buffer.from('foo bar') assert.strictEqual(res.length, 7) res.body = { hello: 'world' } res.remove('Content-Length') assert.strictEqual(res.length, 17) res.body = { hello: 'world' } assert.strictEqual(res.length, 17) res.body = fs.createReadStream('package.json') assert.strictEqual(res.length, undefined) res.body = null assert.strictEqual(res.length, undefined) }) }) describe('and .body is not', () => { it('should return undefined', () => { const res = response() assert.strictEqual(res.length, undefined) }) }) }) describe('and a .type is set to json', () => { describe('and a .body is set to null', () => { it('should return a number', () => { const res = response() res.type = 'json' res.body = null assert.strictEqual(res.length, 4) }) }) }) }) ================================================ FILE: __tests__/response/message.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const response = require('../../test-helpers/context').response describe('res.message', () => { it('should return the response status message', () => { const res = response() res.status = 200 assert.strictEqual(res.message, 'OK') }) describe('when res.message not present', () => { it('should look up in statuses', () => { const res = response() res.res.statusCode = 200 assert.strictEqual(res.message, 'OK') }) }) }) describe('res.message=', () => { it('should set response status message', () => { const res = response() res.status = 200 res.message = 'ok' assert.strictEqual(res.res.statusMessage, 'ok') assert.strictEqual(res.inspect().message, 'ok') }) }) ================================================ FILE: __tests__/response/redirect.test.js ================================================ 'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const request = require('supertest') const context = require('../../test-helpers/context') const Koa = require('../..') describe('ctx.redirect(url)', () => { it('should redirect to the given url', () => { const ctx = context() ctx.redirect('http://google.com') assert.strictEqual(ctx.response.header.location, 'http://google.com/') assert.strictEqual(ctx.status, 302) }) it('should formatting url before redirect', () => { const ctx = context() ctx.redirect('http://google.com\\@apple.com') assert.strictEqual(ctx.response.header.location, 'http://google.com/@apple.com') assert.strictEqual(ctx.status, 302) }) it('should formatting url before redirect', () => { const ctx = context() ctx.redirect('HTTP://google.com\\@apple.com') assert.strictEqual(ctx.response.header.location, 'http://google.com/@apple.com') assert.strictEqual(ctx.status, 302) }) it('should auto fix not encode url', async () => { const app = new Koa() app.use(ctx => { ctx.redirect('http://google.com/😓') }) const res = await request(app.callback()) .get('/') assert.strictEqual(res.status, 302) assert.strictEqual(res.headers.location, 'http://google.com/%F0%9F%98%93') }) describe('when html is accepted', () => { it('should respond with html', () => { const ctx = context() const url = 'http://google.com/' ctx.header.accept = 'text/html' ctx.redirect(url) assert.strictEqual(ctx.response.header['content-type'], 'text/html; charset=utf-8') assert.strictEqual(ctx.body, `Redirecting to ${url}.`) }) it('should escape the url', () => { const ctx = context() let url = '