diff --git a/.eslintrc.yml b/.eslintrc.yml index 20ae2865e..a1c8ab884 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -1,16 +1 @@ -# Support ES2016 features -parser: babel-eslint - -extends: standard - -rules: - arrow-parens: [2, as-needed] - eqeqeq: 0 - no-return-assign: 0 # fails for arrow functions - no-var: 2 - semi: [2, always] - space-before-function-paren: [2, never] - yoda: 0 - arrow-spacing: 2 - dot-location: [2, "property"] - prefer-arrow-callback: 2 +extends: koa diff --git a/.gitignore b/.gitignore index f0e99f947..9df514315 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,3 @@ node_modules test.js coverage npm-debug.log -package-lock.json diff --git a/.mailmap b/.mailmap new file mode 100644 index 000000000..1f2f86feb --- /dev/null +++ b/.mailmap @@ -0,0 +1 @@ +Michał Gołębiowski-Owczarek diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..43c97e719 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/.travis.yml b/.travis.yml index 5410776ee..75e2a966b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,8 @@ language: node_js node_js: - 7 - 8 + - 9 + - 10 cache: directories: - wrk/bin diff --git a/AUTHORS b/AUTHORS index df4fe1f06..d2d048818 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,79 +1,176 @@ -TJ Holowaychuk -Jonathan Ong -dead_horse -Tejas Manohar -Yiyu He -fengmk2 -Julian Gruber -Jonathan Ong -fengmk2 -Rui Marinho -jongleberry -pana +小菜 +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 +George Chung +Gilles De Mey +Grand +Guilherme Pacheco +HanHor Wu +Hartley Melamed +Hrvoje Šimić +Hugh Kennedy Ian Storm Taylor -PatrickJS -Sonny Piers -alsotang +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 -Bryan Bess -Robert Sköld -yoshuawuyts +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 -Aaron Heckmann zensh -Adam L -AlexeyKhristov -Ben Reinhart -C.T. Lin -Chris Tarquini -Christoffer Hallas -Darren Cauthon -Debjeet Biswas -Dmitry Mazuro -Douglas Christopher Wilson -Eivind Fjeldstad -Guilherme Pacheco -HanHor Wu -Hugh Kennedy -Jan Buschtöns -Jan Carlo Viray -Jed Schmidt -Jesus Rodriguez -Jingwei "John" Liu +ziyunfei <446240525@qq.com> +石发磊 diff --git a/History.md b/History.md index 44d942506..a09d6df87 100644 --- a/History.md +++ b/History.md @@ -1,7 +1,68 @@ +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` diff --git a/LICENSE b/LICENSE index 7881c82bd..67c8f12bd 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ (The MIT License) -Copyright (c) 2016 Koa contributors +Copyright (c) 2018 Koa contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/Readme.md b/Readme.md index 074cb0c8e..c352d009e 100644 --- a/Readme.md +++ b/Readme.md @@ -1,4 +1,4 @@ -koa middleware framework for nodejs +Koa middleware framework for nodejs [![gitter][gitter-image]][gitter-url] [![NPM version][npm-image]][npm-url] @@ -22,7 +22,7 @@ Koa requires __node v7.6.0__ or higher for ES2015 and async function support. $ npm install koa ``` -## Hello koa +## Hello Koa ```js const Koa = require('koa'); @@ -38,8 +38,8 @@ app.listen(3000); ## Getting started - - [Kick-Off-Koa](https://github.com/koajs/kick-off-koa) - An intro to koa via a set of self-guided workshops. - - [Workshop](https://github.com/koajs/workshop) - A workshop to learn the basics of koa, Express' spiritual successor. + - [Kick-Off-Koa](https://github.com/koajs/kick-off-koa) - An intro to Koa via a set of self-guided workshops. + - [Workshop](https://github.com/koajs/workshop) - A workshop to learn the basics of Koa, Express' spiritual successor. - [Introduction Screencast](http://knowthen.com/episode-3-koajs-quickstart-guide/) - An introduction to installing and getting started with Koa @@ -195,7 +195,7 @@ the general Koa guide. ## Running tests ``` -$ make test +$ npm test ``` ## Authors @@ -211,7 +211,8 @@ See [AUTHORS](AUTHORS). - [G+ Community](https://plus.google.com/communities/101845768320796750641) - [Reddit Community](https://www.reddit.com/r/koajs) - [Mailing list](https://groups.google.com/forum/#!forum/koajs) - - [中文文档](https://github.com/guo-yu/koa-guide) + - [中文文档 v1.x](https://github.com/guo-yu/koa-guide) + - [中文文档 v2.x](https://github.com/demopark/koa-docs-Zh-CN) - __[#koajs]__ on freenode ## Job Board diff --git a/benchmarks/Makefile b/benchmarks/Makefile index 7faa41026..165cab4d7 100644 --- a/benchmarks/Makefile +++ b/benchmarks/Makefile @@ -2,14 +2,22 @@ all: middleware middleware: - @./run 1 $@ - @./run 5 $@ - @./run 10 $@ - @./run 15 $@ - @./run 20 $@ - @./run 30 $@ - @./run 50 $@ - @./run 100 $@ + @./run 1 false $@ + @./run 5 false $@ + @./run 10 false $@ + @./run 15 false $@ + @./run 20 false $@ + @./run 30 false $@ + @./run 50 false $@ + @./run 100 false $@ + @./run 1 true $@ + @./run 5 true $@ + @./run 10 true $@ + @./run 15 true $@ + @./run 20 true $@ + @./run 30 true $@ + @./run 50 true $@ + @./run 100 true $@ @echo .PHONY: all middleware diff --git a/benchmarks/middleware.js b/benchmarks/middleware.js index bfa854f63..039ccb6f5 100644 --- a/benchmarks/middleware.js +++ b/benchmarks/middleware.js @@ -7,14 +7,24 @@ const app = new Koa(); // number of middleware let n = parseInt(process.env.MW || '1', 10); -console.log(` ${n} middleware`); +let useAsync = process.env.USE_ASYNC === 'true'; + +console.log(` ${n}${useAsync ? ' async' : ''} middleware`); while (n--) { - app.use((ctx, next) => next()); + if (useAsync) { + app.use(async (ctx, next) => await next()); + } else { + app.use((ctx, next) => next()); + } } const body = Buffer.from('Hello World'); -app.use((ctx, next) => next().then(() => ctx.body = body)); +if (useAsync) { + app.use(async (ctx, next) => { await next(); ctx.body = body; }); +} else { + app.use((ctx, next) => next().then(() => ctx.body = body)); +} app.listen(3333); diff --git a/benchmarks/run b/benchmarks/run index 93b5bc52f..b716fa557 100755 --- a/benchmarks/run +++ b/benchmarks/run @@ -1,7 +1,7 @@ #!/usr/bin/env bash echo -MW=$1 node $2 & +MW=$1 USE_ASYNC=$2 node $3 & pid=$! sleep 2 diff --git a/docs/api/context.md b/docs/api/context.md index 3bb0321d9..56d2f326e 100644 --- a/docs/api/context.md +++ b/docs/api/context.md @@ -14,8 +14,8 @@ ```js app.use(async ctx => { ctx; // is the Context - ctx.request; // is a koa Request - ctx.response; // is a koa Response + ctx.request; // is a Koa Request + ctx.response; // is a Koa Response }); ``` @@ -44,11 +44,11 @@ app.use(async ctx => { ### ctx.request - A koa `Request` object. + A Koa `Request` object. ### ctx.response - A koa `Response` object. + A Koa `Response` object. ### ctx.state @@ -68,7 +68,7 @@ ctx.state.user = await User.find(id); - `signed` the cookie requested should be signed -koa uses the [cookies](https://github.com/jed/cookies) module where options are simply passed. +Koa uses the [cookies](https://github.com/jed/cookies) module where options are simply passed. ### ctx.cookies.set(name, value, [options]) @@ -83,7 +83,7 @@ koa uses the [cookies](https://github.com/jed/cookies) module where options are - `httpOnly` server-accessible cookie, __true__ by default - `overwrite` a boolean indicating whether to overwrite previously set cookies of the same name (__false__ by default). If this is true, all cookies set during the same request with the same name (regardless of path or domain) are filtered out of the Set-Cookie header when setting this cookie. -koa uses the [cookies](https://github.com/jed/cookies) module where options are simply passed. +Koa uses the [cookies](https://github.com/jed/cookies) module where options are simply passed. ### ctx.throw([status], [msg], [properties]) @@ -118,7 +118,7 @@ throw err; ctx.throw(401, 'access_denied', { user: user }); ``` -koa uses [http-errors](https://github.com/jshttp/http-errors) to create errors. +Koa uses [http-errors](https://github.com/jshttp/http-errors) to create errors. ### ctx.assert(value, [status], [msg], [properties]) @@ -130,7 +130,7 @@ koa uses [http-errors](https://github.com/jshttp/http-errors) to create errors. ctx.assert(ctx.state.user, 401, 'User not found. Please login!'); ``` -koa uses [http-assert](https://github.com/jshttp/http-assert) for assertions. +Koa uses [http-assert](https://github.com/jshttp/http-assert) for assertions. ### ctx.respond diff --git a/docs/api/index.md b/docs/api/index.md index b27c67799..2ddc283da 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -12,10 +12,10 @@ $ node my-koa-app.js ## Async Functions with Babel -To use `async` functions in Koa in versions of node < 7.6, we recommend using [babel's require hook](http://babeljs.io/docs/usage/require/). +To use `async` functions in Koa in versions of node < 7.6, we recommend using [babel's require hook](http://babeljs.io/docs/usage/babel-register/). ```js -require('babel-core/register'); +require('babel-register'); // require the rest of the app that needs to be transpiled after the hook const app = require('./app'); ``` @@ -80,22 +80,21 @@ app.listen(3000); const Koa = require('koa'); const app = new Koa(); -// x-response-time +// logger app.use(async (ctx, next) => { - const start = Date.now(); await next(); - const ms = Date.now() - start; - ctx.set('X-Response-Time', `${ms}ms`); + const rt = ctx.response.get('X-Response-Time'); + console.log(`${ctx.method} ${ctx.url} - ${rt}`); }); -// logger +// x-response-time app.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; - console.log(`${ctx.method} ${ctx.url} - ${ms}`); + ctx.set('X-Response-Time', `${ms}ms`); }); // response @@ -118,7 +117,7 @@ app.listen(3000); ## app.listen(...) - A Koa application is not a 1-to-1 representation of a HTTP server. + A Koa application is not a 1-to-1 representation of an HTTP server. One or more Koa applications may be mounted together to form larger applications with a single HTTP server. @@ -156,7 +155,7 @@ https.createServer(app.callback()).listen(3001); Return a callback function suitable for the `http.createServer()` method to handle a request. - You may also use this callback function to mount your koa app in a + You may also use this callback function to mount your Koa app in a Connect/Express app. ## app.use(function) @@ -186,7 +185,7 @@ ctx.cookies.set('name', 'tobi', { signed: true }); ## app.context - `app.context` is the prototype from which `ctx` is created from. + `app.context` is the prototype from which `ctx` is created. You may add additional properties to `ctx` by editing `app.context`. This is useful for adding properties or methods to `ctx` to be used across your entire app, which may be more performant (no middleware) and/or easier (fewer `require()`s) @@ -205,12 +204,12 @@ app.use(async ctx => { Note: - Many properties on `ctx` are defined using getters, setters, and `Object.defineProperty()`. You can only edit these properties (not recommended) by using `Object.defineProperty()` on `app.context`. See https://github.com/koajs/koa/issues/652. -- Mounted apps currently use its parent's `ctx` and settings. Thus, mounted apps are really just groups of middleware. +- Mounted apps currently use their parent's `ctx` and settings. Thus, mounted apps are really just groups of middleware. ## Error Handling By default outputs all errors to stderr unless `app.silent` is `true`. - The default error handler also won't outputs errors when `err.status` is `404` or `err.expose` is `true`. + The default error handler also won't output errors when `err.status` is `404` or `err.expose` is `true`. To perform custom error-handling logic such as centralized logging you can add an "error" event listener: ```js diff --git a/docs/guide.md b/docs/guide.md index 668d802a5..4a763ea5d 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -26,37 +26,18 @@ app.use(responseTime); while any code after is the "bubble" phase. This crude gif illustrates how async function allow us to properly utilize stack flow to implement request and response flows: -![koa middleware](/docs/middleware.gif) +![Koa middleware](/docs/middleware.gif) - 1. Create a date to track duration + 1. Create a date to track response time 2. Await control to the next middleware - 3. Create another date to track response time + 3. Create another date to track duration 4. Await control to the next middleware - 5. Await immediately since `contentLength` only works with responses - 6. Await upstream to Koa's noop middleware - 7. Ignore setting the body unless the path is "/" - 8. Set the response to "Hello World" - 9. Ignore setting `Content-Length` when no body is present - 10. Set the field - 11. Output log line - 12. Set `X-Response-Time` header field before response - 13. Hand off to Koa to handle the response - - -Note that the final middleware (step __6__) await to what looks to be nothing — it's actually -yielding to a no-op promise within Koa. This is so that every middleware can conform with the -same API, and may be placed before or after others. If you removed `next();` from the furthest -"downstream" middleware everything would function appropriately, however it would no longer conform -to this behaviour. - - For example this would be fine: - -```js -app.use(async function response(ctx, next) { - if ('/' != this.url) return; - ctx.body = 'Hello World'; -}); -``` + 5. Set the response body to "Hello World" + 6. Calculate duration time + 7. Output log line + 8. Calculate response time + 9. Set `X-Response-Time` header field + 10. Hand off to Koa to handle the response Next we'll look at the best practices for creating Koa middleware. @@ -222,7 +203,7 @@ app.use(async function (ctx, next) { Koa along with many of the libraries it's built with support the __DEBUG__ environment variable from [debug](https://github.com/visionmedia/debug) which provides simple conditional logging. For example - to see all koa-specific debugging information just pass `DEBUG=koa*` and upon boot you'll see the list of middleware used, among other things. + to see all Koa-specific debugging information just pass `DEBUG=koa*` and upon boot you'll see the list of middleware used, among other things. ``` $ DEBUG=koa* node --harmony examples/simple @@ -236,7 +217,7 @@ $ DEBUG=koa* node --harmony examples/simple Since JavaScript does not allow defining function names at runtime, you can also set a middleware's name as `._name`. - This useful when you don't have control of a middleware's name. + This is useful when you don't have control of a middleware's name. For example: ```js diff --git a/docs/koa-vs-express.md b/docs/koa-vs-express.md index 1319a4a7c..1f836efd1 100644 --- a/docs/koa-vs-express.md +++ b/docs/koa-vs-express.md @@ -84,3 +84,12 @@ THIS DOCUMENT IS IN PROGRESS. THIS PARAGRAPH SHALL BE REMOVED WHEN THIS DOCUMENT Better user experience. Proper stream handling. + +### Koa routing (third party libraries support) + + Since Express comes with its own routing, but Koa does not have + any in-built routing, but there are third party libraries available + koa-router and koa-route for routing. + Similarly just like we have helmet for security in Express, for Koa + we have koa-helmet available and the list goes on for Koa third + party available libraries. diff --git a/docs/logo.png b/docs/logo.png new file mode 100644 index 000000000..8a281ea87 Binary files /dev/null and b/docs/logo.png differ diff --git a/docs/middleware.gif b/docs/middleware.gif index 784fc7314..af494b37b 100644 Binary files a/docs/middleware.gif and b/docs/middleware.gif differ diff --git a/docs/migration.md b/docs/migration.md index d6ed25d0e..6dc6ffc2b 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -29,7 +29,7 @@ You don't have to use asynchronous functions - you just have to pass a function A regular function that returns a promise works too! The signature has changed to pass `Context` via an explicit parameter, `ctx` above, instead of via -`this`. The context passing change makes koa more compatible with es6 arrow functions, which capture `this`. +`this`. The context passing change makes Koa more compatible with es6 arrow functions, which capture `this`. ## Using v1.x Middleware in v2.x @@ -75,7 +75,7 @@ Upgrading your middleware may require some work. One migration path is to update 1. Wrap all your current middleware in `koa-convert` 2. Test -3. `npm outdated` to see which koa middleware is outdated +3. `npm outdated` to see which Koa middleware is outdated 4. Update one outdated middleware, remove using `koa-convert` 5. Test 6. Repeat steps 3-5 until you're done diff --git a/lib/application.js b/lib/application.js index a013c964c..84107cb69 100644 --- a/lib/application.js +++ b/lib/application.js @@ -14,10 +14,8 @@ const isJSON = require('koa-is-json'); const context = require('./context'); const request = require('./request'); const statuses = require('statuses'); -const Cookies = require('cookies'); -const accepts = require('accepts'); const Emitter = require('events'); -const assert = require('assert'); +const util = require('util'); const Stream = require('stream'); const http = require('http'); const only = require('only'); @@ -46,6 +44,9 @@ module.exports = class Application extends Emitter { this.context = Object.create(context); this.request = Object.create(request); this.response = Object.create(response); + if (util.inspect.custom) { + this[util.inspect.custom] = this.inspect; + } } /** @@ -125,20 +126,31 @@ module.exports = class Application extends Emitter { callback() { const fn = compose(this.middleware); - if (!this.listeners('error').length) this.on('error', this.onerror); + if (!this.listenerCount('error')) this.on('error', this.onerror); const handleRequest = (req, res) => { - res.statusCode = 404; const ctx = this.createContext(req, res); - const onerror = err => ctx.onerror(err); - const handleResponse = () => respond(ctx); - onFinished(res, onerror); - return fn(ctx).then(handleResponse).catch(onerror); + return this.handleRequest(ctx, fn); }; return handleRequest; } + /** + * Handle request in callback. + * + * @api private + */ + + handleRequest(ctx, fnMiddleware) { + const res = ctx.res; + res.statusCode = 404; + const onerror = err => ctx.onerror(err); + const handleResponse = () => respond(ctx); + onFinished(res, onerror); + return fnMiddleware(ctx).then(handleResponse).catch(onerror); + } + /** * Initialize a new context. * @@ -156,12 +168,6 @@ module.exports = class Application extends Emitter { request.response = response; response.request = request; context.originalUrl = request.originalUrl = req.url; - context.cookies = new Cookies(req, res, { - keys: this.keys, - secure: request.secure - }); - request.ip = request.ips[0] || req.socket.remoteAddress || ''; - context.accept = request.accept = accepts(req); context.state = {}; return context; } @@ -174,7 +180,7 @@ module.exports = class Application extends Emitter { */ onerror(err) { - assert(err instanceof Error, `non-error thrown: ${err}`); + if (!(err instanceof Error)) throw new TypeError(util.format('non-error thrown: %j', err)); if (404 == err.status || err.expose) return; if (this.silent) return; diff --git a/lib/context.js b/lib/context.js index e7b10b849..1b92f018c 100644 --- a/lib/context.js +++ b/lib/context.js @@ -5,10 +5,14 @@ * Module dependencies. */ +const util = require('util'); const createError = require('http-errors'); const httpAssert = require('http-assert'); const delegate = require('delegates'); const statuses = require('statuses'); +const Cookies = require('cookies'); + +const COOKIES = Symbol('context#cookies'); /** * Context prototype. @@ -105,7 +109,7 @@ const proto = module.exports = { // to node-style callbacks. if (null == err) return; - if (!(err instanceof Error)) err = new Error(`non-error thrown: ${err}`); + if (!(err instanceof Error)) err = new Error(util.format('non-error thrown: %j', err)); let headerSent = false; if (this.headerSent || !this.writable) { @@ -125,6 +129,7 @@ const proto = module.exports = { const { res } = this; // first unset all headers + /* istanbul ignore else */ if (typeof res.getHeaderNames === 'function') { res.getHeaderNames().forEach(name => res.removeHeader(name)); } else { @@ -149,9 +154,35 @@ const proto = module.exports = { this.status = err.status; this.length = Buffer.byteLength(msg); this.res.end(msg); + }, + + get cookies() { + if (!this[COOKIES]) { + this[COOKIES] = new Cookies(this.req, this.res, { + keys: this.app.keys, + secure: this.request.secure + }); + } + return this[COOKIES]; + }, + + set cookies(_cookies) { + this[COOKIES] = _cookies; } }; +/** + * Custom inspection implementation for newer Node.js versions. + * + * @return {Object} + * @api public + */ + +/* istanbul ignore else */ +if (util.inspect.custom) { + module.exports[util.inspect.custom] = module.exports.inspect; +} + /** * Response delegation. */ @@ -193,6 +224,7 @@ delegate(proto, 'request') .access('query') .access('path') .access('url') + .access('accept') .getter('origin') .getter('href') .getter('subdomains') diff --git a/lib/request.js b/lib/request.js index 23ed3f23a..9f56410fd 100644 --- a/lib/request.js +++ b/lib/request.js @@ -7,6 +7,7 @@ const URL = require('url').URL; const net = require('net'); +const accepts = require('accepts'); const contentType = require('content-type'); const stringify = require('url').format; const parse = require('parseurl'); @@ -14,6 +15,9 @@ const qs = require('querystring'); const typeis = require('type-is'); const fresh = require('fresh'); const only = require('only'); +const util = require('util'); + +const IP = Symbol('context#ip'); /** * Prototype. @@ -226,7 +230,7 @@ module.exports = { /** * Set the search string. Same as - * response.querystring= but included for ubiquity. + * request.querystring= but included for ubiquity. * * @param {String} str * @api public @@ -278,6 +282,7 @@ module.exports = { */ get URL() { + /* istanbul ignore else */ if (!this.memoizedURL) { const protocol = this.protocol; const host = this.host; @@ -309,7 +314,7 @@ module.exports = { // 2xx or 304 as per rfc2616 14.26 if ((s >= 200 && s < 300) || 304 == s) { - return fresh(this.header, this.ctx.response.header); + return fresh(this.header, this.response.header); } return false; @@ -397,11 +402,10 @@ module.exports = { */ get protocol() { - const proxy = this.app.proxy; if (this.socket.encrypted) return 'https'; - if (!proxy) return 'http'; - const proto = this.get('X-Forwarded-Proto') || 'http'; - return proto.split(/\s*,\s*/)[0]; + if (!this.app.proxy) return 'http'; + const proto = this.get('X-Forwarded-Proto'); + return proto ? proto.split(/\s*,\s*/)[0] : 'http'; }, /** @@ -437,6 +441,26 @@ module.exports = { : []; }, + /** + * Return request's remote address + * When `app.proxy` is `true`, parse + * the "X-Forwarded-For" ip address list and return the first one + * + * @return {String} + * @api public + */ + + get ip() { + if (!this[IP]) { + this[IP] = this.ips[0] || this.socket.remoteAddress || ''; + } + return this[IP]; + }, + + set ip(_ip) { + this[IP] = _ip; + }, + /** * Return subdomains as an array. * @@ -463,6 +487,27 @@ module.exports = { .slice(offset); }, + /** + * Get accept object. + * Lazily memoized. + * + * @return {Object} + * @api private + */ + get accept() { + return this._accept || (this._accept = accepts(this.req)); + }, + + /** + * Set accept object. + * + * @param {Object} + * @api private + */ + set accept(obj) { + return this._accept = obj; + }, + /** * Check if the given `type(s)` is acceptable, returning * the best match when true, otherwise `false`, in which @@ -620,7 +665,7 @@ module.exports = { * // => "text/plain" * * this.get('Something'); - * // => undefined + * // => '' * * @param {String} field * @return {String} @@ -665,3 +710,15 @@ module.exports = { ]); } }; + +/** + * Custom inspection implementation for newer Node.js versions. + * + * @return {Object} + * @api public + */ + +/* istanbul ignore else */ +if (util.inspect.custom) { + module.exports[util.inspect.custom] = module.exports.inspect; +} diff --git a/lib/response.js b/lib/response.js index 504639545..8092c7aad 100644 --- a/lib/response.js +++ b/lib/response.js @@ -7,7 +7,7 @@ const contentDisposition = require('content-disposition'); const ensureErrorHandler = require('error-inject'); -const getType = require('mime-types').contentType; +const getType = require('cache-content-type'); const onFinish = require('on-finished'); const isJSON = require('koa-is-json'); const escape = require('escape-html'); @@ -18,6 +18,7 @@ const assert = require('assert'); const extname = require('path').extname; const vary = require('vary'); const only = require('only'); +const util = require('util'); /** * Prototype. @@ -33,7 +34,7 @@ module.exports = { */ get socket() { - return this.ctx.req.socket; + return this.res.socket; }, /** @@ -80,12 +81,13 @@ module.exports = { */ set status(code) { + if (this.headerSent) return; + assert('number' == typeof code, 'status code must be a number'); assert(statuses[code], `invalid status code: ${code}`); - assert(!this.res.headersSent, 'headers have already been sent'); this._explicitStatus = true; this.res.statusCode = code; - this.res.statusMessage = statuses[code]; + if (this.req.httpVersionMajor < 2) this.res.statusMessage = statuses[code]; if (this.body && statuses.empty[code]) this.body = null; }, @@ -133,8 +135,6 @@ module.exports = { const original = this._body; this._body = val; - if (this.res.headersSent) return; - // no content if (null == val) { if (!statuses.empty[this.status]) this.status = 204; @@ -233,6 +233,8 @@ module.exports = { */ vary(field) { + if (this.headerSent) return; + vary(this.res, field); }, @@ -434,9 +436,11 @@ module.exports = { */ set(field, val) { + if (this.headerSent) return; + if (2 == arguments.length) { - if (Array.isArray(val)) val = val.map(String); - else val = String(val); + if (Array.isArray(val)) val = val.map(v => typeof v === 'string' ? v : String(v)); + else if (typeof val !== 'string') val = String(val); this.res.setHeader(field, val); } else { for (const key in field) { @@ -481,6 +485,8 @@ module.exports = { */ remove(field) { + if (this.headerSent) return; + this.res.removeHeader(field); }, @@ -540,3 +546,13 @@ module.exports = { this.res.flushHeaders(); } }; + +/** + * Custom inspection implementation for newer Node.js versions. + * + * @return {Object} + * @api public + */ +if (util.inspect.custom) { + module.exports[util.inspect.custom] = module.exports.inspect; +} diff --git a/package.json b/package.json index c56356e45..427460962 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,14 @@ { "name": "koa", - "version": "2.3.0", + "version": "2.5.2", "description": "Koa web app framework", "main": "lib/application.js", "scripts": { - "test": "jest --forceExit", - "test-cov": "npm run test -- --coverage", - "lint": "eslint benchmarks lib test --fix", - "bench": "make -C benchmarks" + "test": "jest", + "test-cov": "jest --coverage --runInBand --forceExit", + "lint": "eslint benchmarks lib test", + "bench": "make -C benchmarks", + "authors": "git log --format='%aN <%aE>' | sort -u > AUTHORS" }, "repository": "koajs/koa", "keywords": [ @@ -21,43 +22,42 @@ ], "license": "MIT", "dependencies": { - "accepts": "^1.2.2", - "content-disposition": "~0.5.0", - "content-type": "^1.0.0", - "cookies": "~0.7.0", - "debug": "*", + "accepts": "^1.3.5", + "cache-content-type": "^1.0.0", + "content-disposition": "~0.5.2", + "content-type": "^1.0.4", + "cookies": "~0.7.1", + "debug": "^3.1.0", "delegates": "^1.0.0", - "depd": "^1.1.0", - "destroy": "^1.0.3", - "error-inject": "~1.0.0", - "escape-html": "~1.0.1", - "fresh": "^0.5.0", - "http-assert": "^1.1.0", - "http-errors": "^1.2.8", - "is-generator-function": "^1.0.3", - "koa-compose": "^4.0.0", + "depd": "^1.1.2", + "destroy": "^1.0.4", + "error-inject": "^1.0.0", + "escape-html": "^1.0.3", + "fresh": "~0.5.2", + "http-assert": "^1.3.0", + "http-errors": "^1.6.3", + "is-generator-function": "^1.0.7", + "koa-compose": "^4.1.0", "koa-convert": "^1.2.0", "koa-is-json": "^1.0.0", - "mime-types": "^2.0.7", - "on-finished": "^2.1.0", - "only": "0.0.2", - "parseurl": "^1.3.0", - "statuses": "^1.2.0", - "type-is": "^1.5.5", - "vary": "^1.0.0" + "on-finished": "^2.3.0", + "only": "~0.0.2", + "parseurl": "^1.3.2", + "statuses": "^1.5.0", + "type-is": "^1.6.16", + "vary": "^1.1.2" }, "devDependencies": { - "babel-eslint": "^7.1.1", "eslint": "^3.17.1", + "eslint-config-koa": "^2.0.0", "eslint-config-standard": "^7.0.1", "eslint-plugin-promise": "^3.5.0", "eslint-plugin-standard": "^2.1.1", - "istanbul": "^0.4.0", "jest": "^20.0.0", - "supertest": "^3.0.0" + "supertest": "^3.1.0" }, "engines": { - "node": ">= 6.0.0" + "node": "^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4" }, "files": [ "lib" diff --git a/test/application/index.js b/test/application/index.js index 9fa23b291..51d76ba40 100644 --- a/test/application/index.js +++ b/test/application/index.js @@ -19,7 +19,7 @@ describe('app', () => { done(); }); - request(app.listen()) + request(app.callback()) .get('/') .end(() => {}); }); @@ -41,7 +41,7 @@ describe('app', () => { // hackish, but the response should occur in a single tick setImmediate(done); - request(app.listen()) + request(app.callback()) .get('/') .end(() => {}); }); diff --git a/test/application/inspect.js b/test/application/inspect.js index 8c8aeb4ba..799201ce0 100644 --- a/test/application/inspect.js +++ b/test/application/inspect.js @@ -2,13 +2,20 @@ 'use strict'; const assert = require('assert'); +const util = require('util'); const Koa = require('../..'); +const app = new Koa(); describe('app.inspect()', () => { it('should work', () => { - const app = new Koa(); - const util = require('util'); const str = util.inspect(app); assert.equal("{ subdomainOffset: 2, proxy: false, env: 'test' }", str); }); + + it('should return a json representation', () => { + assert.deepEqual( + { subdomainOffset: 2, proxy: false, env: 'test' }, + app.inspect() + ); + }); }); diff --git a/test/application/onerror.js b/test/application/onerror.js index aaf05471b..ee3d39654 100644 --- a/test/application/onerror.js +++ b/test/application/onerror.js @@ -3,7 +3,6 @@ const assert = require('assert'); const Koa = require('../..'); -const AssertionError = require('assert').AssertionError; describe('app.onerror(err)', () => { beforeEach(() => { @@ -19,7 +18,7 @@ describe('app.onerror(err)', () => { assert.throws(() => { app.onerror('foo'); - }, AssertionError, 'non-error thrown: foo'); + }, TypeError, 'non-error thrown: foo'); }); it('should do nothing if status is 404', () => { diff --git a/test/application/respond.js b/test/application/respond.js index 66021148d..3bc6a74aa 100644 --- a/test/application/respond.js +++ b/test/application/respond.js @@ -39,6 +39,51 @@ describe('app.respond', () => { .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.end('lol'); + ctx.set('foo', 'bar'); + }); + + const server = app.listen(); + + return request(server) + .get('/') + .expect(200) + .expect('lol') + .expect(res => { + assert(!res.headers.foo); + }); + }); + + it('should ignore set status after header sent', () => { + const app = new Koa(); + app.use(ctx => { + ctx.body = 'Hello'; + ctx.respond = false; + + const res = ctx.res; + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + res.end('lol'); + ctx.status = 201; + }); + + const server = app.listen(); + + return request(server) + .get('/') + .expect(200) + .expect('lol'); + }); }); describe('when this.type === null', () => { @@ -639,7 +684,7 @@ describe('app.respond', () => { done(); }); - request(app.listen()) + request(app.callback()) .get('/') .end(() => {}); }); @@ -655,7 +700,7 @@ describe('app.respond', () => { throw err; }); - return request(app.listen()) + return request(app.callback()) .get('/') .expect(403, 'sorry!'); }); @@ -671,7 +716,7 @@ describe('app.respond', () => { throw err; }); - return request(app.listen()) + return request(app.callback()) .get('/') .expect(403, 'Forbidden'); }); diff --git a/test/application/use.js b/test/application/use.js index 0f32c7a61..4d544b7b8 100644 --- a/test/application/use.js +++ b/test/application/use.js @@ -80,7 +80,7 @@ describe('app.use(fn)', () => { app.use(ctx => ctx.throw('Not Found', 404)); - return request(app.listen()) + return request(app.callback()) .get('/') .expect(404); }); @@ -92,7 +92,7 @@ describe('app.use(fn)', () => { app.use((ctx, next) => next()); app.use(function * (next){ this.body = 'generator'; }); - return request(app.listen()) + return request(app.callback()) .get('/') .expect(200) .expect('generator'); diff --git a/test/context/cookies.js b/test/context/cookies.js index 31bf0f1e8..e12df800b 100644 --- a/test/context/cookies.js +++ b/test/context/cookies.js @@ -5,90 +5,115 @@ const assert = require('assert'); const request = require('supertest'); const Koa = require('../..'); -describe('ctx.cookies.set()', () => { - it('should set an unsigned cookie', async () => { - const app = new Koa(); +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; - }); + app.use((ctx, next) => { + ctx.cookies.set('name', 'jon'); + ctx.status = 204; + }); - const server = app.listen(); + const server = app.listen(); - const res = await request(server) - .get('/') - .expect(204); + const res = await request(server) + .get('/') + .expect(204); - const cookie = res.headers['set-cookie'].some(cookie => /^name=/.test(cookie)); - assert.equal(cookie, true); - }); + const cookie = res.headers['set-cookie'].some(cookie => /^name=/.test(cookie)); + assert.equal(cookie, true); + }); - describe('with .signed', () => { - describe('when no .keys are set', () => { - it('should error', () => { + 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) => { - try { - ctx.cookies.set('foo', 'bar', { signed: true }); - } catch (err) { - ctx.body = err.message; - } + ctx.cookies.set('name', 'jon', { signed: true }); + ctx.status = 204; }); - return request(app.listen()) + const server = app.listen(); + + const res = await request(server) .get('/') - .expect('.keys required for signed cookies'); + .expect(204); + + const cookies = res.headers['set-cookie']; + + assert.equal(cookies.some(cookie => /^name=/.test(cookie)), true); + assert.equal(cookies.some(cookie => /(,|^)name\.sig=/.test(cookie)), true); }); }); - it('should send a signed cookie', async () => { - const app = new Koa(); + describe('with secure', () => { + it('should get secure from request', async () => { + const app = new Koa(); - app.keys = ['a', 'b']; + app.proxy = true; + app.keys = ['a', 'b']; - app.use((ctx, next) => { - ctx.cookies.set('name', 'jon', { signed: true }); - ctx.status = 204; - }); - - const server = app.listen(); + app.use(ctx => { + ctx.cookies.set('name', 'jon', { signed: true }); + ctx.status = 204; + }); - const res = await request(server) - .get('/') - .expect(204); + const server = app.listen(); - const cookies = res.headers['set-cookie']; + const res = await request(server) + .get('/') + .set('x-forwarded-proto', 'https') // mock secure + .expect(204); - assert.equal(cookies.some(cookie => /^name=/.test(cookie)), true); - assert.equal(cookies.some(cookie => /(,|^)name\.sig=/.test(cookie)), true); + const cookies = res.headers['set-cookie']; + assert.equal(cookies.some(cookie => /^name=/.test(cookie)), true); + assert.equal(cookies.some(cookie => /(,|^)name\.sig=/.test(cookie)), true); + assert.equal(cookies.every(cookie => /secure/.test(cookie)), true); + }); }); }); - describe('with secure', () => { - it('should get secure from request', async () => { + describe('ctx.cookies=', () => { + it('should override cookie work', async () => { const app = new Koa(); - app.proxy = true; - app.keys = ['a', 'b']; - - app.use(ctx => { - ctx.cookies.set('name', 'jon', { signed: true }); + app.use((ctx, next) => { + ctx.cookies = { + set(key, value){ + ctx.set(key, value); + } + }; + ctx.cookies.set('name', 'jon'); ctx.status = 204; }); const server = app.listen(); - const res = await request(server) + await request(server) .get('/') - .set('x-forwarded-proto', 'https') // mock secure + .expect('name', 'jon') .expect(204); - - const cookies = res.headers['set-cookie']; - assert.equal(cookies.some(cookie => /^name=/.test(cookie)), true); - assert.equal(cookies.some(cookie => /(,|^)name\.sig=/.test(cookie)), true); - assert.equal(cookies.every(cookie => /secure/.test(cookie)), true); }); }); }); diff --git a/test/context/inspect.js b/test/context/inspect.js index 98edf96d9..bb1eb50e5 100644 --- a/test/context/inspect.js +++ b/test/context/inspect.js @@ -3,6 +3,7 @@ const prototype = require('../../lib/context'); const assert = require('assert'); +const util = require('util'); const context = require('../helpers/context'); describe('ctx.inspect()', () => { @@ -11,10 +12,12 @@ describe('ctx.inspect()', () => { const toJSON = ctx.toJSON(ctx); assert.deepEqual(toJSON, ctx.inspect()); + assert.deepEqual(util.inspect(toJSON), util.inspect(ctx)); }); // console.log(require.cache) will call prototype.inspect() it('should not crash when called on the prototype', () => { assert.deepEqual(prototype, prototype.inspect()); + assert.deepEqual(util.inspect(prototype.inspect()), util.inspect(prototype)); }); }); diff --git a/test/context/onerror.js b/test/context/onerror.js index e43331b3d..f1a44a0a8 100644 --- a/test/context/onerror.js +++ b/test/context/onerror.js @@ -102,7 +102,7 @@ describe('ctx.onerror(err)', () => { ctx.body = 'response'; }); - request(app.listen()) + request(app.callback()) .get('/') .expect('X-Foo', 'Bar') .expect(200, () => {}); @@ -129,7 +129,26 @@ describe('ctx.onerror(err)', () => { .expect('Internal Server Error'); }); }); + describe('when ENOENT error', () => { + it('should respond 404', () => { + const app = new Koa(); + + app.use((ctx, next) => { + ctx.body = 'something else'; + const err = new Error('test for ENOENT'); + err.code = 'ENOENT'; + throw err; + }); + const server = app.listen(); + + return request(server) + .get('/') + .expect(404) + .expect('Content-Type', 'text/plain; charset=utf-8') + .expect('Not Found'); + }); + }); describe('not http status code', () => { it('should respond 500', () => { const app = new Koa(); @@ -185,5 +204,23 @@ describe('ctx.onerror(err)', () => { assert.equal(removed, 2); }); + + it('should stringify error if it is an object', done => { + const app = new Koa(); + + app.on('error', err => { + assert.equal(err, 'Error: non-error thrown: {"key":"value"}'); + done(); + }); + + app.use(async ctx => { + throw {key: 'value'}; // eslint-disable-line no-throw-literal + }); + + request(app.callback()) + .get('/') + .expect(500) + .expect('Internal Server Error', () => {}); + }); }); }); diff --git a/test/request/accept.js b/test/request/accept.js new file mode 100644 index 000000000..91781804a --- /dev/null +++ b/test/request/accept.js @@ -0,0 +1,27 @@ + +'use strict'; + +const Accept = require('accepts'); +const assert = require('assert'); +const context = require('../helpers/context'); + +describe('ctx.accept', () => { + it('should return an Accept instance', () => { + const ctx = context(); + ctx.req.headers.accept = 'application/*;q=0.2, image/jpeg;q=0.8, text/html, text/plain'; + assert(ctx.accept instanceof Accept); + }); +}); + +describe('ctx.accept=', () => { + it('should replace the accept object', () => { + const ctx = context(); + ctx.req.headers.accept = 'text/plain'; + assert.deepEqual(ctx.accepts(), ['text/plain']); + + const request = context.request(); + request.req.headers.accept = 'application/*;q=0.2, image/jpeg;q=0.8, text/html, text/plain'; + ctx.accept = Accept(request.req); + assert.deepEqual(ctx.accepts(), ['text/html', 'text/plain', 'image/jpeg', 'application/*']); + }); +}); diff --git a/test/request/inspect.js b/test/request/inspect.js index 393f219ef..7d63dea8a 100644 --- a/test/request/inspect.js +++ b/test/request/inspect.js @@ -3,6 +3,7 @@ const request = require('../helpers/context').request; const assert = require('assert'); +const util = require('util'); describe('req.inspect()', () => { describe('with no request.req present', () => { @@ -10,7 +11,8 @@ describe('req.inspect()', () => { const req = request(); req.method = 'GET'; delete req.req; - assert(null == req.inspect()); + assert(undefined === req.inspect()); + assert('undefined' === util.inspect(req)); }); }); @@ -20,12 +22,15 @@ describe('req.inspect()', () => { req.url = 'example.com'; req.header.host = 'example.com'; - assert.deepEqual({ + const expected = { method: 'GET', url: 'example.com', header: { host: 'example.com' } - }, req.inspect()); + }; + + assert.deepEqual(req.inspect(), expected); + assert.deepEqual(util.inspect(req), util.inspect(expected)); }); }); diff --git a/test/request/ip.js b/test/request/ip.js index 8c8cb09ea..3375f700b 100644 --- a/test/request/ip.js +++ b/test/request/ip.js @@ -39,11 +39,21 @@ describe('req.ip', () => { }); }); - it('should be cached', () => { + it('should be lazy inited and cached', () => { const req = { socket: new Stream.Duplex() }; req.socket.remoteAddress = '127.0.0.2'; const request = Request(req); + assert.equal(request.ip, '127.0.0.2'); req.socket.remoteAddress = '127.0.0.1'; assert.equal(request.ip, '127.0.0.2'); }); + + it('should reset ip work', () => { + const req = { socket: new Stream.Duplex() }; + req.socket.remoteAddress = '127.0.0.2'; + const request = Request(req); + assert.equal(request.ip, '127.0.0.2'); + request.ip = '127.0.0.1'; + assert.equal(request.ip, '127.0.0.1'); + }); }); diff --git a/test/request/whatwg-url.js b/test/request/whatwg-url.js index af13e4cc1..7a52a6910 100644 --- a/test/request/whatwg-url.js +++ b/test/request/whatwg-url.js @@ -7,14 +7,15 @@ const assert = require('assert'); describe('req.URL', () => { describe('should not throw when', () => { it('host is void', () => { - const req = request(); - assert.doesNotThrow(() => req.URL, TypeError); + // Accessing the URL should not throw. + request().URL; }); it('header.host is invalid', () => { const req = request(); req.header.host = 'invalid host'; - assert.doesNotThrow(() => req.URL, TypeError); + // Accessing the URL should not throw. + req.URL; }); }); diff --git a/test/response/attachment.js b/test/response/attachment.js index 41f4eee11..5e0f732a9 100644 --- a/test/response/attachment.js +++ b/test/response/attachment.js @@ -40,7 +40,7 @@ describe('ctx.attachment([filename])', () => { ctx.body = {foo: 'bar'}; }); - return request(app.listen()) + 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'}) diff --git a/test/response/flushHeaders.js b/test/response/flushHeaders.js index 176db2fa0..b1c5de684 100644 --- a/test/response/flushHeaders.js +++ b/test/response/flushHeaders.js @@ -61,39 +61,27 @@ describe('ctx.flushHeaders()', () => { .expect('Body'); }); - it('should fail to set the headers after flushHeaders', async () => { + 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 = ''; - try { - ctx.set('X-Shouldnt-Work', 'Value'); - } catch (err) { - ctx.body += 'ctx.set fail '; - } - try { - ctx.status = 200; - } catch (err) { - ctx.body += 'ctx.status fail '; - } - try { - ctx.length = 10; - } catch (err) { - ctx.body += 'ctx.length fail'; - } + ctx.body = 'foo'; + ctx.set('X-Shouldnt-Work', 'Value'); + ctx.remove('Content-Type'); + ctx.vary('Content-Type'); }); const server = app.listen(); const res = await request(server) .get('/') .expect(401) - .expect('Content-Type', 'text/plain') - .expect('ctx.set fail ctx.status fail ctx.length fail'); + .expect('Content-Type', 'text/plain'); assert.equal(res.headers['x-shouldnt-work'], undefined, 'header set after flushHeaders'); + assert.equal(res.headers.vary, undefined, 'header set after flushHeaders'); }); it('should flush headers first and delay to send data', done => { @@ -134,4 +122,30 @@ describe('ctx.flushHeaders()', () => { .end(); }); }); + + it('should catch stream error', done => { + const PassThrough = require('stream').PassThrough; + const app = new Koa(); + app.once('error', err => { + assert(err.message === 'mock error'); + done(); + }); + + 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')); + }, 100); + }); + + const server = app.listen(); + + request(server).get('/').end(); + }); }); diff --git a/test/response/header.js b/test/response/header.js index dabbd8784..b7ccfc1b8 100644 --- a/test/response/header.js +++ b/test/response/header.js @@ -10,7 +10,8 @@ describe('res.header', () => { it('should return the response header object', () => { const res = response(); res.set('X-Foo', 'bar'); - assert.deepEqual(res.header, { 'x-foo': 'bar' }); + res.set('X-Number', 200); + assert.deepEqual(res.header, { 'x-foo': 'bar', 'x-number': '200' }); }); it('should use res.getHeaders() accessor when available', () => { @@ -29,7 +30,7 @@ describe('res.header', () => { header = Object.assign({}, ctx.response.header); }); - await request(app.listen()) + await request(app.callback()) .get('/'); assert.deepEqual(header, { 'x-foo': '42' }); diff --git a/test/response/inspect.js b/test/response/inspect.js index 8f42d3afa..be7cbeb2b 100644 --- a/test/response/inspect.js +++ b/test/response/inspect.js @@ -3,6 +3,7 @@ const response = require('../helpers/context').response; const assert = require('assert'); +const util = require('util'); describe('res.inspect()', () => { describe('with no response.res present', () => { @@ -11,6 +12,7 @@ describe('res.inspect()', () => { res.body = 'hello'; delete res.res; assert.equal(res.inspect(), null); + assert.equal(util.inspect(res), 'undefined'); }); }); @@ -18,14 +20,17 @@ describe('res.inspect()', () => { const res = response(); res.body = 'hello'; - assert.deepEqual({ - body: 'hello', + const expected = { status: 200, message: 'OK', header: { - 'content-length': '5', - 'content-type': 'text/plain; charset=utf-8' - } - }, res.inspect()); + 'content-type': 'text/plain; charset=utf-8', + 'content-length': '5' + }, + body: 'hello' + }; + + assert.deepEqual(res.inspect(), expected); + assert.deepEqual(util.inspect(res), util.inspect(expected)); }); }); diff --git a/test/response/set.js b/test/response/set.js index 8a21a057e..710b4586d 100644 --- a/test/response/set.js +++ b/test/response/set.js @@ -11,12 +11,18 @@ describe('ctx.set(name, val)', () => { assert.equal(ctx.response.header['x-foo'], 'bar'); }); - it('should coerce to a string', () => { + it('should coerce number to string', () => { const ctx = context(); ctx.set('x-foo', 5); assert.equal(ctx.response.header['x-foo'], '5'); }); + it('should coerce undefined to string', () => { + const ctx = context(); + ctx.set('x-foo', undefined); + assert.equal(ctx.response.header['x-foo'], 'undefined'); + }); + it('should set a field value of array', () => { const ctx = context(); ctx.set('x-foo', ['foo', 'bar']); diff --git a/test/response/status.js b/test/response/status.js index d2e7884c7..63e2ff687 100644 --- a/test/response/status.js +++ b/test/response/status.js @@ -17,9 +17,7 @@ describe('res.status=', () => { }); it('should not throw', () => { - assert.doesNotThrow(() => { - response().status = 403; - }); + response().status = 403; }); }); @@ -27,7 +25,7 @@ describe('res.status=', () => { it('should throw', () => { assert.throws(() => { response().status = 999; - }, 'invalid status code: 999'); + }, /invalid status code: 999/); }); }); @@ -41,14 +39,25 @@ describe('res.status=', () => { }); it('should not throw', () => { - assert.doesNotThrow(() => response().status = 700); + response().status = 700; + }); + }); + + describe('and HTTP/2', () => { + it('should not set the status message', () => { + const res = response({ + 'httpVersionMajor': 2, + 'httpVersion': '2.0' + }); + res.status = 200; + assert(!res.res.statusMessage); }); }); }); describe('when a status string', () => { it('should throw', () => { - assert.throws(() => response().status = 'forbidden', 'status code must be a number'); + assert.throws(() => response().status = 'forbidden', /status code must be a number/); }); }); @@ -67,7 +76,7 @@ describe('res.status=', () => { assert(null == ctx.response.header['transfer-encoding']); }); - const res = await request(app.listen()) + const res = await request(app.callback()) .get('/') .expect(status); @@ -88,7 +97,7 @@ describe('res.status=', () => { ctx.set('Transfer-Encoding', 'chunked'); }); - const res = await request(app.listen()) + const res = await request(app.callback()) .get('/') .expect(status);