add koa Request / Response objects and delegation. Closes #52
This commit is contained in:
parent
4cc5b30fb1
commit
c699c75c52
38 changed files with 2967 additions and 2280 deletions
3
Makefile
3
Makefile
|
@ -3,6 +3,9 @@ test:
|
||||||
@NODE_ENV=test ./node_modules/.bin/mocha \
|
@NODE_ENV=test ./node_modules/.bin/mocha \
|
||||||
--require should \
|
--require should \
|
||||||
--harmony-generators \
|
--harmony-generators \
|
||||||
|
test/context/* \
|
||||||
|
test/request/* \
|
||||||
|
test/response/* \
|
||||||
--bail
|
--bail
|
||||||
|
|
||||||
bench:
|
bench:
|
||||||
|
|
|
@ -26,7 +26,7 @@ alias node='node --harmony-generators'
|
||||||
|
|
||||||
## Community
|
## Community
|
||||||
|
|
||||||
- [API](docs/api.md) documentation
|
- [API](docs/api/index.md) documentation
|
||||||
- [Middleware](https://github.com/koajs/koa/wiki) list
|
- [Middleware](https://github.com/koajs/koa/wiki) list
|
||||||
- [Wiki](https://github.com/koajs/koa/wiki)
|
- [Wiki](https://github.com/koajs/koa/wiki)
|
||||||
- [G+ Community](https://plus.google.com/communities/101845768320796750641)
|
- [G+ Community](https://plus.google.com/communities/101845768320796750641)
|
||||||
|
|
661
docs/api.md
661
docs/api.md
|
@ -1,661 +0,0 @@
|
||||||
## Application
|
|
||||||
|
|
||||||
A Koa application is not a 1-to-1 representation of an HTTP server,
|
|
||||||
as one or more Koa applications may be mounted together to form larger
|
|
||||||
applications, with a single HTTP server.
|
|
||||||
|
|
||||||
The following is a useless Koa application bound to port `3000`:
|
|
||||||
|
|
||||||
```js
|
|
||||||
var koa = require('koa');
|
|
||||||
var app = koa();
|
|
||||||
app.listen(3000);
|
|
||||||
```
|
|
||||||
|
|
||||||
The `app.listen(...)` method is simply sugar for the following:
|
|
||||||
|
|
||||||
```js
|
|
||||||
var http = require('http');
|
|
||||||
var koa = require('koa');
|
|
||||||
var app = koa();
|
|
||||||
http.createServer(app.callback()).listen(3000);
|
|
||||||
```
|
|
||||||
|
|
||||||
This means you can spin up the same application as both HTTP and HTTPS,
|
|
||||||
or on multiple addresses:
|
|
||||||
|
|
||||||
```js
|
|
||||||
var http = require('http');
|
|
||||||
var koa = require('koa');
|
|
||||||
var app = koa();
|
|
||||||
http.createServer(app.callback()).listen(3000);
|
|
||||||
http.createServer(app.callback()).listen(3001);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Settings
|
|
||||||
|
|
||||||
Application settings are properties on the `app` instance, currently
|
|
||||||
the following are supported:
|
|
||||||
|
|
||||||
- `app.name` optionally give your application a name
|
|
||||||
- `app.env` defaulting to the __NODE_ENV__ or "development"
|
|
||||||
- `app.proxy` when true proxy header fields will be trusted
|
|
||||||
- `app.subdomainOffset` offset of `.subdomains` to ignore [2]
|
|
||||||
- `app.jsonSpaces` default JSON response spaces [2]
|
|
||||||
- `app.outputErrors` output err.stack to stderr [false in "test" environment]
|
|
||||||
|
|
||||||
### app.listen(...)
|
|
||||||
|
|
||||||
Create and return an HTTP server, passing the given arguments to
|
|
||||||
`Server#listen()`. These arguments are documented on [nodejs.org](http://nodejs.org/api/http.html#http_server_listen_port_hostname_backlog_callback).
|
|
||||||
|
|
||||||
### app.callback()
|
|
||||||
|
|
||||||
Return a callback function suitable for the `http.createServer()`
|
|
||||||
method to handle a request.
|
|
||||||
|
|
||||||
### app.use(function)
|
|
||||||
|
|
||||||
Add the given middleware function to this application. See [Middleware](#middleware) for
|
|
||||||
more information.
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
A Koa Context encapsulates node's `request` and `response` objects
|
|
||||||
into a single object which provides many helpful methods for writing
|
|
||||||
web applications and APIs.
|
|
||||||
|
|
||||||
These operations are used so frequently in HTTP server development
|
|
||||||
that they are added at this level, instead of a higher level framework,
|
|
||||||
which would force middlware to re-implement this common functionality.
|
|
||||||
|
|
||||||
A `Context` is created _per_ request, and is referenced in middleware
|
|
||||||
as the receiver, or the `this` variable.
|
|
||||||
|
|
||||||
### ctx.req
|
|
||||||
|
|
||||||
Node's `request` object.
|
|
||||||
|
|
||||||
### ctx.res
|
|
||||||
|
|
||||||
Node's `response` object.
|
|
||||||
|
|
||||||
### ctx.app
|
|
||||||
|
|
||||||
Application instance reference.
|
|
||||||
|
|
||||||
### ctx.header
|
|
||||||
|
|
||||||
Request header object.
|
|
||||||
|
|
||||||
### ctx.responseHeader
|
|
||||||
|
|
||||||
Response header object.
|
|
||||||
|
|
||||||
### ctx.method
|
|
||||||
|
|
||||||
Request method.
|
|
||||||
|
|
||||||
### ctx.method=
|
|
||||||
|
|
||||||
Set request method, useful for implementing middleware
|
|
||||||
such as `methodOverride()`.
|
|
||||||
|
|
||||||
### ctx.status
|
|
||||||
|
|
||||||
Get response status.
|
|
||||||
|
|
||||||
### ctx.status=
|
|
||||||
|
|
||||||
Set response status via numeric code or case-insensitive string:
|
|
||||||
|
|
||||||
- 100 "continue"
|
|
||||||
- 101 "switching protocols"
|
|
||||||
- 102 "processing"
|
|
||||||
- 200 "ok"
|
|
||||||
- 201 "created"
|
|
||||||
- 202 "accepted"
|
|
||||||
- 203 "non-authoritative information"
|
|
||||||
- 204 "no content"
|
|
||||||
- 205 "reset content"
|
|
||||||
- 206 "partial content"
|
|
||||||
- 207 "multi-status"
|
|
||||||
- 300 "multiple choices"
|
|
||||||
- 301 "moved permanently"
|
|
||||||
- 302 "moved temporarily"
|
|
||||||
- 303 "see other"
|
|
||||||
- 304 "not modified"
|
|
||||||
- 305 "use proxy"
|
|
||||||
- 307 "temporary redirect"
|
|
||||||
- 400 "bad request"
|
|
||||||
- 401 "unauthorized"
|
|
||||||
- 402 "payment required"
|
|
||||||
- 403 "forbidden"
|
|
||||||
- 404 "not found"
|
|
||||||
- 405 "method not allowed"
|
|
||||||
- 406 "not acceptable"
|
|
||||||
- 407 "proxy authentication required"
|
|
||||||
- 408 "request time-out"
|
|
||||||
- 409 "conflict"
|
|
||||||
- 410 "gone"
|
|
||||||
- 411 "length required"
|
|
||||||
- 412 "precondition failed"
|
|
||||||
- 413 "request entity too large"
|
|
||||||
- 414 "request-uri too large"
|
|
||||||
- 415 "unsupported media type"
|
|
||||||
- 416 "requested range not satisfiable"
|
|
||||||
- 417 "expectation failed"
|
|
||||||
- 418 "i'm a teapot"
|
|
||||||
- 422 "unprocessable entity"
|
|
||||||
- 423 "locked"
|
|
||||||
- 424 "failed dependency"
|
|
||||||
- 425 "unordered collection"
|
|
||||||
- 426 "upgrade required"
|
|
||||||
- 428 "precondition required"
|
|
||||||
- 429 "too many requests"
|
|
||||||
- 431 "request header fields too large"
|
|
||||||
- 500 "internal server error"
|
|
||||||
- 501 "not implemented"
|
|
||||||
- 502 "bad gateway"
|
|
||||||
- 503 "service unavailable"
|
|
||||||
- 504 "gateway time-out"
|
|
||||||
- 505 "http version not supported"
|
|
||||||
- 506 "variant also negotiates"
|
|
||||||
- 507 "insufficient storage"
|
|
||||||
- 509 "bandwidth limit exceeded"
|
|
||||||
- 510 "not extended"
|
|
||||||
- 511 "network authentication required"
|
|
||||||
|
|
||||||
__NOTE__: don't worry too much about memorizing these strings,
|
|
||||||
if you have a typo an error will be thrown, displaying this list
|
|
||||||
so you can make a correction.
|
|
||||||
|
|
||||||
### ctx.length=
|
|
||||||
|
|
||||||
Set response Content-Length to the given value.
|
|
||||||
|
|
||||||
### ctx.length
|
|
||||||
|
|
||||||
Return request Content-Length as a number when present, or undefined.
|
|
||||||
|
|
||||||
### ctx.responseLength
|
|
||||||
|
|
||||||
Return response Content-Length as a number when present, or deduce
|
|
||||||
from `ctx.body` when possible, or undefined.
|
|
||||||
|
|
||||||
### ctx.body
|
|
||||||
|
|
||||||
Get response body. When `ctx.body` is `null` and `ctx.status` is still
|
|
||||||
200 it is considered a 404. This is to prevent the developer from manually
|
|
||||||
specifying `this.status = 200` on every response.
|
|
||||||
|
|
||||||
### ctx.body=
|
|
||||||
|
|
||||||
Set response body to one of the following:
|
|
||||||
|
|
||||||
- `string` written
|
|
||||||
- `Buffer` written
|
|
||||||
- `Stream` piped
|
|
||||||
- `Object` json-stringified
|
|
||||||
- `null` no content response
|
|
||||||
|
|
||||||
#### String
|
|
||||||
|
|
||||||
The Content-Type is defaulted to text/html or text/plain, both with
|
|
||||||
a default charset of utf-8. The Content-Length field is also set.
|
|
||||||
|
|
||||||
#### Buffer
|
|
||||||
|
|
||||||
The Content-Type is defaulted to application/octet-stream, and Content-Length
|
|
||||||
is also set.
|
|
||||||
|
|
||||||
#### Stream
|
|
||||||
|
|
||||||
The Content-Type is defaulted to application/octet-stream.
|
|
||||||
|
|
||||||
#### Object
|
|
||||||
|
|
||||||
The Content-Type is defaulted to application/json.
|
|
||||||
|
|
||||||
#### Notes
|
|
||||||
|
|
||||||
To alter the JSON response formatting use the `app.jsonSpaces`
|
|
||||||
setting, for example to compress JSON responses set:
|
|
||||||
|
|
||||||
```js
|
|
||||||
app.jsonSpaces = 0;
|
|
||||||
```
|
|
||||||
|
|
||||||
### ctx.get(field)
|
|
||||||
|
|
||||||
Get a request header field value with case-insensitive `field`.
|
|
||||||
|
|
||||||
```js
|
|
||||||
var etag = this.get('If-None-Match');
|
|
||||||
```
|
|
||||||
|
|
||||||
### ctx.set(field, value)
|
|
||||||
|
|
||||||
Set response header `field` to `value`:
|
|
||||||
|
|
||||||
```js
|
|
||||||
this.set('Cache-Control', 'no-cache');
|
|
||||||
```
|
|
||||||
|
|
||||||
### ctx.set(fields)
|
|
||||||
|
|
||||||
Set several response header `fields` with an object:
|
|
||||||
|
|
||||||
```js
|
|
||||||
this.set({
|
|
||||||
'Etag': '1234',
|
|
||||||
'Last-Modified': date
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### ctx.type
|
|
||||||
|
|
||||||
Get request `Content-Type` void of parameters such as "charset".
|
|
||||||
|
|
||||||
```js
|
|
||||||
var ct = this.type;
|
|
||||||
// => "image/png"
|
|
||||||
```
|
|
||||||
|
|
||||||
### ctx.type=
|
|
||||||
|
|
||||||
Set response `Content-Type` via mime string or file extension.
|
|
||||||
|
|
||||||
```js
|
|
||||||
this.type = 'text/plain; charset=utf-8';
|
|
||||||
this.type = 'image/png';
|
|
||||||
this.type = '.png';
|
|
||||||
this.type = 'png';
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: when appropriate a `charset` is selected for you, for
|
|
||||||
example `ctx.type = 'html'` will default to "utf-8", however
|
|
||||||
when explicitly defined in full as `ctx.type = 'text/html'`
|
|
||||||
no charset is assigned.
|
|
||||||
|
|
||||||
### ctx.url
|
|
||||||
|
|
||||||
Get request URL.
|
|
||||||
|
|
||||||
### ctx.url=
|
|
||||||
|
|
||||||
Set request URL, useful for url rewrites.
|
|
||||||
|
|
||||||
### ctx.path
|
|
||||||
|
|
||||||
Get request pathname.
|
|
||||||
|
|
||||||
### ctx.path=
|
|
||||||
|
|
||||||
Set request pathname and retain query-string when present.
|
|
||||||
|
|
||||||
### ctx.query
|
|
||||||
|
|
||||||
Get parsed query-string, returning an empty object when no
|
|
||||||
query-string is present. Note that this getter does _not_
|
|
||||||
support nested parsing.
|
|
||||||
|
|
||||||
For example "color=blue&size=small":
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
color: 'blue',
|
|
||||||
size: 'small'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### ctx.query=
|
|
||||||
|
|
||||||
Set query-string to the given object. Note that this
|
|
||||||
setter does _not_ support nested objects.
|
|
||||||
|
|
||||||
```js
|
|
||||||
this.query = { next: '/login' };
|
|
||||||
```
|
|
||||||
|
|
||||||
### ctx.querystring
|
|
||||||
|
|
||||||
Get raw query string void of `?`.
|
|
||||||
|
|
||||||
### ctx.querystring=
|
|
||||||
|
|
||||||
Set raw query string.
|
|
||||||
|
|
||||||
### ctx.host
|
|
||||||
|
|
||||||
Get host void of port number when present. Supports `X-Forwarded-Host`
|
|
||||||
when `app.proxy` is __true__, otherwise `Host` is used.
|
|
||||||
|
|
||||||
### ctx.fresh
|
|
||||||
|
|
||||||
Check if a request cache is "fresh", aka the contents have not changed. This
|
|
||||||
method is for cache negotiation between `If-None-Match` / `ETag`, and `If-Modified-Since` and `Last-Modified`. It should be referenced after setting one or more of these response headers.
|
|
||||||
|
|
||||||
```js
|
|
||||||
this.set('ETag', '123');
|
|
||||||
|
|
||||||
// cache is ok
|
|
||||||
if (this.fresh) {
|
|
||||||
this.status = 304;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// cache is stale
|
|
||||||
// fetch new data
|
|
||||||
this.body = yield db.find('something');
|
|
||||||
```
|
|
||||||
|
|
||||||
### ctx.stale
|
|
||||||
|
|
||||||
Inverse of `ctx.fresh`.
|
|
||||||
|
|
||||||
### ctx.protocol
|
|
||||||
|
|
||||||
Return request protocol, "https" or "http". Supports `X-Forwarded-Proto`
|
|
||||||
when `app.proxy` is __true__.
|
|
||||||
|
|
||||||
### ctx.secure
|
|
||||||
|
|
||||||
Shorthand for `this.protocol == "https"` to check if a requset was
|
|
||||||
issued via TLS.
|
|
||||||
|
|
||||||
### ctx.ip
|
|
||||||
|
|
||||||
Request remote address. Supports `X-Forwarded-For` when `app.proxy`
|
|
||||||
is __true__.
|
|
||||||
|
|
||||||
### ctx.ips
|
|
||||||
|
|
||||||
When `X-Forwarded-For` is present and `app.proxy` is enabled an array
|
|
||||||
of these ips is returned, ordered from upstream -> downstream. When disabled
|
|
||||||
an empty array is returned.
|
|
||||||
|
|
||||||
### ctx.subdomains
|
|
||||||
|
|
||||||
Return subdomains as an array.
|
|
||||||
|
|
||||||
Subdomains are the dot-separated parts of the host before the main domain of
|
|
||||||
the app. By default, the domain of the app is assumed to be the last two
|
|
||||||
parts of the host. This can be changed by setting `app.subdomainOffset`.
|
|
||||||
|
|
||||||
For example, if the domain is "tobi.ferrets.example.com":
|
|
||||||
If `app.subdomainOffset` is not set, this.subdomains is `["ferrets", "tobi"]`.
|
|
||||||
If `app.subdomainOffset` is 3, this.subdomains is `["tobi"]`.
|
|
||||||
|
|
||||||
### ctx.is(type)
|
|
||||||
|
|
||||||
Check if the incoming request contains the `Content-Type`
|
|
||||||
header field, and it contains the give mime `type`.
|
|
||||||
|
|
||||||
```js
|
|
||||||
// With Content-Type: text/html; charset=utf-8
|
|
||||||
this.is('html');
|
|
||||||
this.is('.html');
|
|
||||||
this.is('text/html');
|
|
||||||
this.is('text/*');
|
|
||||||
// => true
|
|
||||||
|
|
||||||
// When Content-Type is application/json
|
|
||||||
this.is('json');
|
|
||||||
this.is('.json');
|
|
||||||
this.is('application/json');
|
|
||||||
this.is('application/*');
|
|
||||||
// => true
|
|
||||||
|
|
||||||
this.is('html');
|
|
||||||
// => false
|
|
||||||
```
|
|
||||||
|
|
||||||
### ctx.redirect(url, [alt])
|
|
||||||
|
|
||||||
Perform a 302 redirect to `url`.
|
|
||||||
|
|
||||||
The string "back" is special-cased
|
|
||||||
to provide Referrer support, when Referrer
|
|
||||||
is not present `alt` or "/" is used.
|
|
||||||
|
|
||||||
```js
|
|
||||||
this.redirect('back');
|
|
||||||
this.redirect('back', '/index.html');
|
|
||||||
this.redirect('/login');
|
|
||||||
this.redirect('http://google.com');
|
|
||||||
```
|
|
||||||
|
|
||||||
To alter the default status of `302` or the response
|
|
||||||
body simply re-assign after this call:
|
|
||||||
|
|
||||||
```js
|
|
||||||
this.redirect('/cart');
|
|
||||||
this.status = 301;
|
|
||||||
this.body = 'Redirecting to shopping cart';
|
|
||||||
```
|
|
||||||
|
|
||||||
### ctx.attachment([filename])
|
|
||||||
|
|
||||||
Set `Content-Disposition` to "attachment" to signal the client
|
|
||||||
to prompt for download. Optionally specify the `filename` of the
|
|
||||||
download.
|
|
||||||
|
|
||||||
### ctx.accepts(types)
|
|
||||||
|
|
||||||
Check if the given `type(s)` is acceptable, returning
|
|
||||||
the best match when true, otherwise `undefined`, in which
|
|
||||||
case you should respond with 406 "Not Acceptable".
|
|
||||||
|
|
||||||
The `type` value may be one or more mime type string
|
|
||||||
such as "application/json", the extension name
|
|
||||||
such as "json", or an array `["json", "html", "text/plain"]`. When a list or array is given the _best_ match, if any is returned.
|
|
||||||
|
|
||||||
```js
|
|
||||||
// Accept: text/html
|
|
||||||
this.accepts('html');
|
|
||||||
// => "html"
|
|
||||||
|
|
||||||
// Accept: text/*, application/json
|
|
||||||
this.accepts('html');
|
|
||||||
// => "html"
|
|
||||||
this.accepts('text/html');
|
|
||||||
// => "text/html"
|
|
||||||
this.accepts('json', 'text');
|
|
||||||
// => "json"
|
|
||||||
this.accepts('application/json');
|
|
||||||
// => "application/json"
|
|
||||||
|
|
||||||
// Accept: text/*, application/json
|
|
||||||
this.accepts('image/png');
|
|
||||||
this.accepts('png');
|
|
||||||
// => undefined
|
|
||||||
|
|
||||||
// Accept: text/*;q=.5, application/json
|
|
||||||
this.accepts(['html', 'json']);
|
|
||||||
this.accepts('html', 'json');
|
|
||||||
// => "json"
|
|
||||||
```
|
|
||||||
|
|
||||||
You may call `this.accepts()` as may times as you like,
|
|
||||||
or use a switch:
|
|
||||||
|
|
||||||
```js
|
|
||||||
switch (this.accepts('json', 'html', 'text')) {
|
|
||||||
case 'json': break;
|
|
||||||
case 'html': break;
|
|
||||||
case 'text': break;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### ctx.acceptsEncodings(encodings)
|
|
||||||
|
|
||||||
Check if `encodings` are acceptable, returning
|
|
||||||
the best match when true, otherwise `undefined`.
|
|
||||||
|
|
||||||
```js
|
|
||||||
// Accept-Encoding: gzip
|
|
||||||
this.acceptsEncodings('gzip', 'deflate');
|
|
||||||
// => "gzip"
|
|
||||||
|
|
||||||
this.acceptsEncodings(['gzip', 'deflate']);
|
|
||||||
// => "gzip"
|
|
||||||
```
|
|
||||||
|
|
||||||
When no arguments are given all accepted encodings
|
|
||||||
are returned as an array:
|
|
||||||
|
|
||||||
```js
|
|
||||||
// Accept-Encoding: gzip, deflate
|
|
||||||
this.acceptsEncodings();
|
|
||||||
// => ["gzip", "deflate"]
|
|
||||||
```
|
|
||||||
|
|
||||||
### ctx.acceptsCharsets(charsets)
|
|
||||||
|
|
||||||
Check if `charsets` are acceptable, returning
|
|
||||||
the best match when true, otherwise `undefined`.
|
|
||||||
|
|
||||||
```js
|
|
||||||
// Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5
|
|
||||||
this.acceptsCharsets('utf-8', 'utf-7');
|
|
||||||
// => "utf-8"
|
|
||||||
|
|
||||||
this.acceptsCharsets(['utf-7', 'utf-8']);
|
|
||||||
// => "utf-8"
|
|
||||||
```
|
|
||||||
|
|
||||||
When no arguments are given all accepted charsets
|
|
||||||
are returned as an array:
|
|
||||||
|
|
||||||
```js
|
|
||||||
// Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5
|
|
||||||
this.acceptsCharsets();
|
|
||||||
// => ["utf-8", "utf-7", "iso-8859-1"]
|
|
||||||
```
|
|
||||||
|
|
||||||
### ctx.acceptsLanguages(langs)
|
|
||||||
|
|
||||||
Check if `langs` are acceptable, returning
|
|
||||||
the best match when true, otherwise `undefined`.
|
|
||||||
|
|
||||||
```js
|
|
||||||
// Accept-Language: en;q=0.8, es, pt
|
|
||||||
this.acceptsLanguages('es', 'en');
|
|
||||||
// => "es"
|
|
||||||
|
|
||||||
this.acceptsLanguages(['en', 'es']);
|
|
||||||
// => "es"
|
|
||||||
```
|
|
||||||
|
|
||||||
When no arguments are given all accepted languages
|
|
||||||
are returned as an array:
|
|
||||||
|
|
||||||
```js
|
|
||||||
// Accept-Language: en;q=0.8, es, pt
|
|
||||||
this.acceptsLanguages();
|
|
||||||
// => ["es", "pt", "en"]
|
|
||||||
```
|
|
||||||
|
|
||||||
### ctx.headerSent
|
|
||||||
|
|
||||||
Check if a response header has already been sent. Useful for seeing
|
|
||||||
if the client may be notified on error.
|
|
||||||
|
|
||||||
### ctx.cookies.get(name, [options])
|
|
||||||
|
|
||||||
Get cookie `name` with `options`:
|
|
||||||
|
|
||||||
- `signed` the cookie requested should be signed
|
|
||||||
|
|
||||||
### ctx.cookies.set(name, value, [options])
|
|
||||||
|
|
||||||
Set cookie `name` to `value` with `options`:
|
|
||||||
|
|
||||||
- `signed` sign the cookie value
|
|
||||||
- `expires` a `Date` for cookie expiration
|
|
||||||
- `path` cookie path, `/'` by default
|
|
||||||
- `domain` cookie domain
|
|
||||||
- `secure` secure cookie
|
|
||||||
- `httpOnly` server-accessible cookie, __true__ by default
|
|
||||||
|
|
||||||
### ctx.socket
|
|
||||||
|
|
||||||
Request socket object.
|
|
||||||
|
|
||||||
### ctx.error(msg, [status])
|
|
||||||
|
|
||||||
Helper method to throw an error with a `.status` property
|
|
||||||
that will allow Koa to respond appropriately. The following
|
|
||||||
combinations are allowed:
|
|
||||||
|
|
||||||
```js
|
|
||||||
this.error(403)
|
|
||||||
this.error('name required', 400)
|
|
||||||
this.error('something exploded')
|
|
||||||
```
|
|
||||||
|
|
||||||
For example `this.error('name required', 400)` is requivalent to:
|
|
||||||
|
|
||||||
```js
|
|
||||||
var err = new Error('name required');
|
|
||||||
err.status = 400;
|
|
||||||
throw err;
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that these are user-level errors and are flagged with
|
|
||||||
`err.expose` meaning the messages are appropriate for
|
|
||||||
client responses, which is typically not the case for
|
|
||||||
error messages since you do not want to leak failure
|
|
||||||
details.
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
By default outputs all errors to stderr unless __NODE_ENV__ is "test". To perform custom error-handling logic such as centralized logging you
|
|
||||||
can add an "error" event listener:
|
|
||||||
|
|
||||||
```js
|
|
||||||
app.on('error', function(err){
|
|
||||||
log.error('server error', err);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
If an error in the req/res cycle and it is _not_ possible to respond to the client, the `Context` instance is also passed:
|
|
||||||
|
|
||||||
```js
|
|
||||||
app.on('error', function(err){
|
|
||||||
log.error('server error', err);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
When an error occurs _and_ it is still possible to respond to the client, aka no data has been written to the socket, Koa will respond
|
|
||||||
appropriately with a 500 "Internal Server Error". In either case
|
|
||||||
an app-level "error" is emitted for logging purposes.
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
### HEAD Support
|
|
||||||
|
|
||||||
Koa's upstream response middleware supports __HEAD__ for you,
|
|
||||||
however expensive requests would benefit from custom handling. For
|
|
||||||
example instead of reading a file into memory and piping it to the
|
|
||||||
client, you may wish to `stat()` and set the `Content-*` header fields
|
|
||||||
appropriately to bypass the read.
|
|
||||||
|
|
||||||
### Socket Errors
|
|
||||||
|
|
||||||
Node http servers emit a "clientError" event when a socket error occurs. You'll probably want to delegate this to your
|
|
||||||
Koa handler by doing the following, in order to centralize
|
|
||||||
logging:
|
|
||||||
|
|
||||||
```js
|
|
||||||
var app = koa();
|
|
||||||
var srv = app.listen(3000);
|
|
||||||
srv.on('clientError', function(err){
|
|
||||||
app.emit('error', err);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
# License
|
|
||||||
|
|
||||||
MIT
|
|
132
docs/api/context.md
Normal file
132
docs/api/context.md
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
|
||||||
|
# Context
|
||||||
|
|
||||||
|
A Koa Context encapsulates node's `request` and `response` objects
|
||||||
|
into a single object which provides many helpful methods for writing
|
||||||
|
web applications and APIs.
|
||||||
|
|
||||||
|
Many accesors and methods simply delegate to their `ctx.request` or `ctx.response`
|
||||||
|
equivalents for convenience, and are otherwise identical.
|
||||||
|
|
||||||
|
These operations are used so frequently in HTTP server development
|
||||||
|
that they are added at this level, instead of a higher level framework,
|
||||||
|
which would force middlware to re-implement this common functionality.
|
||||||
|
|
||||||
|
A `Context` is created _per_ request, and is referenced in middleware
|
||||||
|
as the receiver, or the `this` identifier.
|
||||||
|
|
||||||
|
## Request aliases
|
||||||
|
|
||||||
|
The following accessors and alias [Request](request.md) equivalents:
|
||||||
|
|
||||||
|
- `ctx.header`
|
||||||
|
- `ctx.method`
|
||||||
|
- `ctx.method=`
|
||||||
|
- `ctx.url`
|
||||||
|
- `ctx.url=`
|
||||||
|
- `ctx.path`
|
||||||
|
- `ctx.path=`
|
||||||
|
- `ctx.query`
|
||||||
|
- `ctx.query=`
|
||||||
|
- `ctx.querystring`
|
||||||
|
- `ctx.querystring=`
|
||||||
|
- `ctx.length`
|
||||||
|
- `ctx.host`
|
||||||
|
- `ctx.fresh`
|
||||||
|
- `ctx.stale`
|
||||||
|
- `ctx.socket`
|
||||||
|
- `ctx.protocol`
|
||||||
|
- `ctx.secure`
|
||||||
|
- `ctx.ip`
|
||||||
|
- `ctx.ips`
|
||||||
|
- `ctx.subdomains`
|
||||||
|
- `ctx.is()`
|
||||||
|
- `ctx.accepts()`
|
||||||
|
- `ctx.acceptsEncodings()`
|
||||||
|
- `ctx.acceptsCharsets()`
|
||||||
|
- `ctx.acceptsLanguages()`
|
||||||
|
- `ctx.get()`
|
||||||
|
|
||||||
|
## Response aliases
|
||||||
|
|
||||||
|
The following accessors and alias [Response](response.md) equivalents:
|
||||||
|
|
||||||
|
- `ctx.body`
|
||||||
|
- `ctx.body=`
|
||||||
|
- `ctx.status`
|
||||||
|
- `ctx.status=`
|
||||||
|
- `ctx.length=`
|
||||||
|
- `ctx.type`
|
||||||
|
- `ctx.type=`
|
||||||
|
- `ctx.headerSent`
|
||||||
|
- `ctx.redirect()`
|
||||||
|
- `ctx.attachment()`
|
||||||
|
- `ctx.set()`
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
`Context` specific methods and accessors.
|
||||||
|
|
||||||
|
### ctx.req
|
||||||
|
|
||||||
|
Node's `request` object.
|
||||||
|
|
||||||
|
### ctx.res
|
||||||
|
|
||||||
|
Node's `response` object.
|
||||||
|
|
||||||
|
### ctx.request
|
||||||
|
|
||||||
|
A koa `Request` object.
|
||||||
|
|
||||||
|
### ctx.response
|
||||||
|
|
||||||
|
A koa `Response` object.
|
||||||
|
|
||||||
|
### ctx.app
|
||||||
|
|
||||||
|
Application instance reference.
|
||||||
|
|
||||||
|
### ctx.cookies.get(name, [options])
|
||||||
|
|
||||||
|
Get cookie `name` with `options`:
|
||||||
|
|
||||||
|
- `signed` the cookie requested should be signed
|
||||||
|
|
||||||
|
### ctx.cookies.set(name, value, [options])
|
||||||
|
|
||||||
|
Set cookie `name` to `value` with `options`:
|
||||||
|
|
||||||
|
- `signed` sign the cookie value
|
||||||
|
- `expires` a `Date` for cookie expiration
|
||||||
|
- `path` cookie path, `/'` by default
|
||||||
|
- `domain` cookie domain
|
||||||
|
- `secure` secure cookie
|
||||||
|
- `httpOnly` server-accessible cookie, __true__ by default
|
||||||
|
|
||||||
|
### ctx.error(msg, [status])
|
||||||
|
|
||||||
|
Helper method to throw an error with a `.status` property
|
||||||
|
that will allow Koa to respond appropriately. The following
|
||||||
|
combinations are allowed:
|
||||||
|
|
||||||
|
```js
|
||||||
|
this.error(403)
|
||||||
|
this.error('name required', 400)
|
||||||
|
this.error('something exploded')
|
||||||
|
```
|
||||||
|
|
||||||
|
For example `this.error('name required', 400)` is requivalent to:
|
||||||
|
|
||||||
|
```js
|
||||||
|
var err = new Error('name required');
|
||||||
|
err.status = 400;
|
||||||
|
throw err;
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that these are user-level errors and are flagged with
|
||||||
|
`err.expose` meaning the messages are appropriate for
|
||||||
|
client responses, which is typically not the case for
|
||||||
|
error messages since you do not want to leak failure
|
||||||
|
details.
|
||||||
|
|
119
docs/api/index.md
Normal file
119
docs/api/index.md
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
## Application
|
||||||
|
|
||||||
|
A Koa application is not a 1-to-1 representation of an HTTP server,
|
||||||
|
as one or more Koa applications may be mounted together to form larger
|
||||||
|
applications, with a single HTTP server.
|
||||||
|
|
||||||
|
The following is a useless Koa application bound to port `3000`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
var koa = require('koa');
|
||||||
|
var app = koa();
|
||||||
|
app.listen(3000);
|
||||||
|
```
|
||||||
|
|
||||||
|
The `app.listen(...)` method is simply sugar for the following:
|
||||||
|
|
||||||
|
```js
|
||||||
|
var http = require('http');
|
||||||
|
var koa = require('koa');
|
||||||
|
var app = koa();
|
||||||
|
http.createServer(app.callback()).listen(3000);
|
||||||
|
```
|
||||||
|
|
||||||
|
This means you can spin up the same application as both HTTP and HTTPS,
|
||||||
|
or on multiple addresses:
|
||||||
|
|
||||||
|
```js
|
||||||
|
var http = require('http');
|
||||||
|
var koa = require('koa');
|
||||||
|
var app = koa();
|
||||||
|
http.createServer(app.callback()).listen(3000);
|
||||||
|
http.createServer(app.callback()).listen(3001);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Settings
|
||||||
|
|
||||||
|
Application settings are properties on the `app` instance, currently
|
||||||
|
the following are supported:
|
||||||
|
|
||||||
|
- `app.name` optionally give your application a name
|
||||||
|
- `app.env` defaulting to the __NODE_ENV__ or "development"
|
||||||
|
- `app.proxy` when true proxy header fields will be trusted
|
||||||
|
- `app.subdomainOffset` offset of `.subdomains` to ignore [2]
|
||||||
|
- `app.jsonSpaces` default JSON response spaces [2]
|
||||||
|
- `app.outputErrors` output err.stack to stderr [false in "test" environment]
|
||||||
|
|
||||||
|
### app.listen(...)
|
||||||
|
|
||||||
|
Create and return an HTTP server, passing the given arguments to
|
||||||
|
`Server#listen()`. These arguments are documented on [nodejs.org](http://nodejs.org/api/http.html#http_server_listen_port_hostname_backlog_callback).
|
||||||
|
|
||||||
|
### app.callback()
|
||||||
|
|
||||||
|
Return a callback function suitable for the `http.createServer()`
|
||||||
|
method to handle a request.
|
||||||
|
|
||||||
|
### app.use(function)
|
||||||
|
|
||||||
|
Add the given middleware function to this application. See [Middleware](#middleware) for
|
||||||
|
more information.
|
||||||
|
|
||||||
|
## Handling Requests
|
||||||
|
|
||||||
|
Koa requests are manipulated using a `Context` object containing both a Koa `Request` and `Response` object. For more information on these view:
|
||||||
|
|
||||||
|
- [Context](context.md)
|
||||||
|
- [Request](request.md)
|
||||||
|
- [Response](response.md)
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
By default outputs all errors to stderr unless __NODE_ENV__ is "test". To perform custom error-handling logic such as centralized logging you
|
||||||
|
can add an "error" event listener:
|
||||||
|
|
||||||
|
```js
|
||||||
|
app.on('error', function(err){
|
||||||
|
log.error('server error', err);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
If an error in the req/res cycle and it is _not_ possible to respond to the client, the `Context` instance is also passed:
|
||||||
|
|
||||||
|
```js
|
||||||
|
app.on('error', function(err){
|
||||||
|
log.error('server error', err);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
When an error occurs _and_ it is still possible to respond to the client, aka no data has been written to the socket, Koa will respond
|
||||||
|
appropriately with a 500 "Internal Server Error". In either case
|
||||||
|
an app-level "error" is emitted for logging purposes.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
### HEAD Support
|
||||||
|
|
||||||
|
Koa's upstream response middleware supports __HEAD__ for you,
|
||||||
|
however expensive requests would benefit from custom handling. For
|
||||||
|
example instead of reading a file into memory and piping it to the
|
||||||
|
client, you may wish to `stat()` and set the `Content-*` header fields
|
||||||
|
appropriately to bypass the read.
|
||||||
|
|
||||||
|
### Socket Errors
|
||||||
|
|
||||||
|
Node http servers emit a "clientError" event when a socket error occurs. You'll probably want to delegate this to your
|
||||||
|
Koa handler by doing the following, in order to centralize
|
||||||
|
logging:
|
||||||
|
|
||||||
|
```js
|
||||||
|
var app = koa();
|
||||||
|
var srv = app.listen(3000);
|
||||||
|
srv.on('clientError', function(err){
|
||||||
|
app.emit('error', err);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
# License
|
||||||
|
|
||||||
|
MIT
|
309
docs/api/request.md
Normal file
309
docs/api/request.md
Normal file
|
@ -0,0 +1,309 @@
|
||||||
|
|
||||||
|
# Request
|
||||||
|
|
||||||
|
A Koa `Request` object is an abstraction on top of node's vanilla request object,
|
||||||
|
providing additional functionality that is useful for every day HTTP server
|
||||||
|
development.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### req.header
|
||||||
|
|
||||||
|
Request header object.
|
||||||
|
|
||||||
|
### req.method
|
||||||
|
|
||||||
|
Request method.
|
||||||
|
|
||||||
|
### req.method=
|
||||||
|
|
||||||
|
Set request method, useful for implementing middleware
|
||||||
|
such as `methodOverride()`.
|
||||||
|
|
||||||
|
### req.length
|
||||||
|
|
||||||
|
Return request Content-Length as a number when present, or undefined.
|
||||||
|
|
||||||
|
### req.type
|
||||||
|
|
||||||
|
Get request `Content-Type` void of parameters such as "charset".
|
||||||
|
|
||||||
|
```js
|
||||||
|
var ct = this.type;
|
||||||
|
// => "image/png"
|
||||||
|
```
|
||||||
|
|
||||||
|
### req.url
|
||||||
|
|
||||||
|
Get request URL.
|
||||||
|
|
||||||
|
### req.url=
|
||||||
|
|
||||||
|
Set request URL, useful for url rewrites.
|
||||||
|
|
||||||
|
### req.path
|
||||||
|
|
||||||
|
Get request pathname.
|
||||||
|
|
||||||
|
### req.path=
|
||||||
|
|
||||||
|
Set request pathname and retain query-string when present.
|
||||||
|
|
||||||
|
### req.query
|
||||||
|
|
||||||
|
Get parsed query-string, returning an empty object when no
|
||||||
|
query-string is present. Note that this getter does _not_
|
||||||
|
support nested parsing.
|
||||||
|
|
||||||
|
For example "color=blue&size=small":
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
color: 'blue',
|
||||||
|
size: 'small'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### req.query=
|
||||||
|
|
||||||
|
Set query-string to the given object. Note that this
|
||||||
|
setter does _not_ support nested objects.
|
||||||
|
|
||||||
|
```js
|
||||||
|
this.query = { next: '/login' };
|
||||||
|
```
|
||||||
|
|
||||||
|
### req.querystring
|
||||||
|
|
||||||
|
Get raw query string void of `?`.
|
||||||
|
|
||||||
|
### req.querystring=
|
||||||
|
|
||||||
|
Set raw query string.
|
||||||
|
|
||||||
|
### req.host
|
||||||
|
|
||||||
|
Get host void of port number when present. Supports `X-Forwarded-Host`
|
||||||
|
when `app.proxy` is __true__, otherwise `Host` is used.
|
||||||
|
|
||||||
|
### req.fresh
|
||||||
|
|
||||||
|
Check if a request cache is "fresh", aka the contents have not changed. This
|
||||||
|
method is for cache negotiation between `If-None-Match` / `ETag`, and `If-Modified-Since` and `Last-Modified`. It should be referenced after setting one or more of these response headers.
|
||||||
|
|
||||||
|
```js
|
||||||
|
this.set('ETag', '123');
|
||||||
|
|
||||||
|
// cache is ok
|
||||||
|
if (this.fresh) {
|
||||||
|
this.status = 304;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// cache is stale
|
||||||
|
// fetch new data
|
||||||
|
this.body = yield db.find('something');
|
||||||
|
```
|
||||||
|
|
||||||
|
### req.stale
|
||||||
|
|
||||||
|
Inverse of `req.fresh`.
|
||||||
|
|
||||||
|
### req.protocol
|
||||||
|
|
||||||
|
Return request protocol, "https" or "http". Supports `X-Forwarded-Proto`
|
||||||
|
when `app.proxy` is __true__.
|
||||||
|
|
||||||
|
### req.secure
|
||||||
|
|
||||||
|
Shorthand for `this.protocol == "https"` to check if a requset was
|
||||||
|
issued via TLS.
|
||||||
|
|
||||||
|
### req.ip
|
||||||
|
|
||||||
|
Request remote address. Supports `X-Forwarded-For` when `app.proxy`
|
||||||
|
is __true__.
|
||||||
|
|
||||||
|
### req.ips
|
||||||
|
|
||||||
|
When `X-Forwarded-For` is present and `app.proxy` is enabled an array
|
||||||
|
of these ips is returned, ordered from upstream -> downstream. When disabled
|
||||||
|
an empty array is returned.
|
||||||
|
|
||||||
|
### req.subdomains
|
||||||
|
|
||||||
|
Return subdomains as an array.
|
||||||
|
|
||||||
|
Subdomains are the dot-separated parts of the host before the main domain of
|
||||||
|
the app. By default, the domain of the app is assumed to be the last two
|
||||||
|
parts of the host. This can be changed by setting `app.subdomainOffset`.
|
||||||
|
|
||||||
|
For example, if the domain is "tobi.ferrets.example.com":
|
||||||
|
If `app.subdomainOffset` is not set, this.subdomains is `["ferrets", "tobi"]`.
|
||||||
|
If `app.subdomainOffset` is 3, this.subdomains is `["tobi"]`.
|
||||||
|
|
||||||
|
### req.is(type)
|
||||||
|
|
||||||
|
Check if the incoming request contains the `Content-Type`
|
||||||
|
header field, and it contains the give mime `type`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// With Content-Type: text/html; charset=utf-8
|
||||||
|
this.is('html');
|
||||||
|
this.is('.html');
|
||||||
|
this.is('text/html');
|
||||||
|
this.is('text/*');
|
||||||
|
// => true
|
||||||
|
|
||||||
|
// When Content-Type is application/json
|
||||||
|
this.is('json');
|
||||||
|
this.is('.json');
|
||||||
|
this.is('application/json');
|
||||||
|
this.is('application/*');
|
||||||
|
// => true
|
||||||
|
|
||||||
|
this.is('html');
|
||||||
|
// => false
|
||||||
|
```
|
||||||
|
|
||||||
|
### req.accepts(types)
|
||||||
|
|
||||||
|
Check if the given `type(s)` is acceptable, returning
|
||||||
|
the best match when true, otherwise `undefined`, in which
|
||||||
|
case you should respond with 406 "Not Acceptable".
|
||||||
|
|
||||||
|
The `type` value may be one or more mime type string
|
||||||
|
such as "application/json", the extension name
|
||||||
|
such as "json", or an array `["json", "html", "text/plain"]`. When a list or array is given the _best_ match, if any is returned.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Accept: text/html
|
||||||
|
this.accepts('html');
|
||||||
|
// => "html"
|
||||||
|
|
||||||
|
// Accept: text/*, application/json
|
||||||
|
this.accepts('html');
|
||||||
|
// => "html"
|
||||||
|
this.accepts('text/html');
|
||||||
|
// => "text/html"
|
||||||
|
this.accepts('json', 'text');
|
||||||
|
// => "json"
|
||||||
|
this.accepts('application/json');
|
||||||
|
// => "application/json"
|
||||||
|
|
||||||
|
// Accept: text/*, application/json
|
||||||
|
this.accepts('image/png');
|
||||||
|
this.accepts('png');
|
||||||
|
// => undefined
|
||||||
|
|
||||||
|
// Accept: text/*;q=.5, application/json
|
||||||
|
this.accepts(['html', 'json']);
|
||||||
|
this.accepts('html', 'json');
|
||||||
|
// => "json"
|
||||||
|
```
|
||||||
|
|
||||||
|
You may call `this.accepts()` as may times as you like,
|
||||||
|
or use a switch:
|
||||||
|
|
||||||
|
```js
|
||||||
|
switch (this.accepts('json', 'html', 'text')) {
|
||||||
|
case 'json': break;
|
||||||
|
case 'html': break;
|
||||||
|
case 'text': break;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### req.acceptsEncodings(encodings)
|
||||||
|
|
||||||
|
Check if `encodings` are acceptable, returning
|
||||||
|
the best match when true, otherwise `undefined`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Accept-Encoding: gzip
|
||||||
|
this.acceptsEncodings('gzip', 'deflate');
|
||||||
|
// => "gzip"
|
||||||
|
|
||||||
|
this.acceptsEncodings(['gzip', 'deflate']);
|
||||||
|
// => "gzip"
|
||||||
|
```
|
||||||
|
|
||||||
|
When no arguments are given all accepted encodings
|
||||||
|
are returned as an array:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Accept-Encoding: gzip, deflate
|
||||||
|
this.acceptsEncodings();
|
||||||
|
// => ["gzip", "deflate"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### req.acceptsCharsets(charsets)
|
||||||
|
|
||||||
|
Check if `charsets` are acceptable, returning
|
||||||
|
the best match when true, otherwise `undefined`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5
|
||||||
|
this.acceptsCharsets('utf-8', 'utf-7');
|
||||||
|
// => "utf-8"
|
||||||
|
|
||||||
|
this.acceptsCharsets(['utf-7', 'utf-8']);
|
||||||
|
// => "utf-8"
|
||||||
|
```
|
||||||
|
|
||||||
|
When no arguments are given all accepted charsets
|
||||||
|
are returned as an array:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5
|
||||||
|
this.acceptsCharsets();
|
||||||
|
// => ["utf-8", "utf-7", "iso-8859-1"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### req.acceptsLanguages(langs)
|
||||||
|
|
||||||
|
Check if `langs` are acceptable, returning
|
||||||
|
the best match when true, otherwise `undefined`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Accept-Language: en;q=0.8, es, pt
|
||||||
|
this.acceptsLanguages('es', 'en');
|
||||||
|
// => "es"
|
||||||
|
|
||||||
|
this.acceptsLanguages(['en', 'es']);
|
||||||
|
// => "es"
|
||||||
|
```
|
||||||
|
|
||||||
|
When no arguments are given all accepted languages
|
||||||
|
are returned as an array:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Accept-Language: en;q=0.8, es, pt
|
||||||
|
this.acceptsLanguages();
|
||||||
|
// => ["es", "pt", "en"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### req.error(msg, [status])
|
||||||
|
|
||||||
|
Helper method to throw an error with a `.status` property
|
||||||
|
that will allow Koa to respond appropriately. The following
|
||||||
|
combinations are allowed:
|
||||||
|
|
||||||
|
```js
|
||||||
|
this.error(403)
|
||||||
|
this.error('name required', 400)
|
||||||
|
this.error('something exploded')
|
||||||
|
```
|
||||||
|
|
||||||
|
For example `this.error('name required', 400)` is requivalent to:
|
||||||
|
|
||||||
|
```js
|
||||||
|
var err = new Error('name required');
|
||||||
|
err.status = 400;
|
||||||
|
throw err;
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that these are user-level errors and are flagged with
|
||||||
|
`err.expose` meaning the messages are appropriate for
|
||||||
|
client responses, which is typically not the case for
|
||||||
|
error messages since you do not want to leak failure
|
||||||
|
details.
|
221
docs/api/response.md
Normal file
221
docs/api/response.md
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
|
||||||
|
# Response
|
||||||
|
|
||||||
|
A Koa `Response` object is an abstraction on top of node's vanilla response object,
|
||||||
|
providing additional functionality that is useful for every day HTTP server
|
||||||
|
development.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### res.header
|
||||||
|
|
||||||
|
Response header object.
|
||||||
|
|
||||||
|
### res.status
|
||||||
|
|
||||||
|
Get response status.
|
||||||
|
|
||||||
|
### res.status=
|
||||||
|
|
||||||
|
Set response status via numeric code or case-insensitive string:
|
||||||
|
|
||||||
|
- 100 "continue"
|
||||||
|
- 101 "switching protocols"
|
||||||
|
- 102 "processing"
|
||||||
|
- 200 "ok"
|
||||||
|
- 201 "created"
|
||||||
|
- 202 "accepted"
|
||||||
|
- 203 "non-authoritative information"
|
||||||
|
- 204 "no content"
|
||||||
|
- 205 "reset content"
|
||||||
|
- 206 "partial content"
|
||||||
|
- 207 "multi-status"
|
||||||
|
- 300 "multiple choices"
|
||||||
|
- 301 "moved permanently"
|
||||||
|
- 302 "moved temporarily"
|
||||||
|
- 303 "see other"
|
||||||
|
- 304 "not modified"
|
||||||
|
- 305 "use proxy"
|
||||||
|
- 307 "temporary redirect"
|
||||||
|
- 400 "bad request"
|
||||||
|
- 401 "unauthorized"
|
||||||
|
- 402 "payment required"
|
||||||
|
- 403 "forbidden"
|
||||||
|
- 404 "not found"
|
||||||
|
- 405 "method not allowed"
|
||||||
|
- 406 "not acceptable"
|
||||||
|
- 407 "proxy authentication required"
|
||||||
|
- 408 "request time-out"
|
||||||
|
- 409 "conflict"
|
||||||
|
- 410 "gone"
|
||||||
|
- 411 "length required"
|
||||||
|
- 412 "precondition failed"
|
||||||
|
- 413 "request entity too large"
|
||||||
|
- 414 "request-uri too large"
|
||||||
|
- 415 "unsupported media type"
|
||||||
|
- 416 "requested range not satisfiable"
|
||||||
|
- 417 "expectation failed"
|
||||||
|
- 418 "i'm a teapot"
|
||||||
|
- 422 "unprocessable entity"
|
||||||
|
- 423 "locked"
|
||||||
|
- 424 "failed dependency"
|
||||||
|
- 425 "unordered collection"
|
||||||
|
- 426 "upgrade required"
|
||||||
|
- 428 "precondition required"
|
||||||
|
- 429 "too many requests"
|
||||||
|
- 431 "request header fields too large"
|
||||||
|
- 500 "internal server error"
|
||||||
|
- 501 "not implemented"
|
||||||
|
- 502 "bad gateway"
|
||||||
|
- 503 "service unavailable"
|
||||||
|
- 504 "gateway time-out"
|
||||||
|
- 505 "http version not supported"
|
||||||
|
- 506 "variant also negotiates"
|
||||||
|
- 507 "insufficient storage"
|
||||||
|
- 509 "bandwidth limit exceeded"
|
||||||
|
- 510 "not extended"
|
||||||
|
- 511 "network authentication required"
|
||||||
|
|
||||||
|
__NOTE__: don't worry too much about memorizing these strings,
|
||||||
|
if you have a typo an error will be thrown, displaying this list
|
||||||
|
so you can make a correction.
|
||||||
|
|
||||||
|
### res.length=
|
||||||
|
|
||||||
|
Set response Content-Length to the given value.
|
||||||
|
|
||||||
|
### res.length
|
||||||
|
|
||||||
|
Return response Content-Length as a number when present, or deduce
|
||||||
|
from `res.body` when possible, or undefined.
|
||||||
|
|
||||||
|
### res.body
|
||||||
|
|
||||||
|
Get response body. When `res.body` is `null` and `res.status` is still
|
||||||
|
200 it is considered a 404. This is to prevent the developer from manually
|
||||||
|
specifying `this.status = 200` on every response.
|
||||||
|
|
||||||
|
### res.body=
|
||||||
|
|
||||||
|
Set response body to one of the following:
|
||||||
|
|
||||||
|
- `string` written
|
||||||
|
- `Buffer` written
|
||||||
|
- `Stream` piped
|
||||||
|
- `Object` json-stringified
|
||||||
|
- `null` no content response
|
||||||
|
|
||||||
|
#### String
|
||||||
|
|
||||||
|
The Content-Type is defaulted to text/html or text/plain, both with
|
||||||
|
a default charset of utf-8. The Content-Length field is also set.
|
||||||
|
|
||||||
|
#### Buffer
|
||||||
|
|
||||||
|
The Content-Type is defaulted to application/octet-stream, and Content-Length
|
||||||
|
is also set.
|
||||||
|
|
||||||
|
#### Stream
|
||||||
|
|
||||||
|
The Content-Type is defaulted to application/octet-stream.
|
||||||
|
|
||||||
|
#### Object
|
||||||
|
|
||||||
|
The Content-Type is defaulted to application/json.
|
||||||
|
|
||||||
|
#### Notes
|
||||||
|
|
||||||
|
To alter the JSON response formatting use the `app.jsonSpaces`
|
||||||
|
setting, for example to compress JSON responses set:
|
||||||
|
|
||||||
|
```js
|
||||||
|
app.jsonSpaces = 0;
|
||||||
|
```
|
||||||
|
|
||||||
|
### res.get(field)
|
||||||
|
|
||||||
|
Get a response header field value with case-insensitive `field`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var etag = this.get('ETag');
|
||||||
|
```
|
||||||
|
|
||||||
|
### res.set(field, value)
|
||||||
|
|
||||||
|
Set response header `field` to `value`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
this.set('Cache-Control', 'no-cache');
|
||||||
|
```
|
||||||
|
|
||||||
|
### res.set(fields)
|
||||||
|
|
||||||
|
Set several response header `fields` with an object:
|
||||||
|
|
||||||
|
```js
|
||||||
|
this.set({
|
||||||
|
'Etag': '1234',
|
||||||
|
'Last-Modified': date
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### res.type
|
||||||
|
|
||||||
|
Get response `Content-Type` void of parameters such as "charset".
|
||||||
|
|
||||||
|
```js
|
||||||
|
var ct = this.type;
|
||||||
|
// => "image/png"
|
||||||
|
```
|
||||||
|
|
||||||
|
### res.type=
|
||||||
|
|
||||||
|
Set response `Content-Type` via mime string or file extension.
|
||||||
|
|
||||||
|
```js
|
||||||
|
this.type = 'text/plain; charset=utf-8';
|
||||||
|
this.type = 'image/png';
|
||||||
|
this.type = '.png';
|
||||||
|
this.type = 'png';
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: when appropriate a `charset` is selected for you, for
|
||||||
|
example `res.type = 'html'` will default to "utf-8", however
|
||||||
|
when explicitly defined in full as `res.type = 'text/html'`
|
||||||
|
no charset is assigned.
|
||||||
|
|
||||||
|
### res.redirect(url, [alt])
|
||||||
|
|
||||||
|
Perform a 302 redirect to `url`.
|
||||||
|
|
||||||
|
The string "back" is special-cased
|
||||||
|
to provide Referrer support, when Referrer
|
||||||
|
is not present `alt` or "/" is used.
|
||||||
|
|
||||||
|
```js
|
||||||
|
this.redirect('back');
|
||||||
|
this.redirect('back', '/index.html');
|
||||||
|
this.redirect('/login');
|
||||||
|
this.redirect('http://google.com');
|
||||||
|
```
|
||||||
|
|
||||||
|
To alter the default status of `302` or the response
|
||||||
|
body simply re-assign after this call:
|
||||||
|
|
||||||
|
```js
|
||||||
|
this.redirect('/cart');
|
||||||
|
this.status = 301;
|
||||||
|
this.body = 'Redirecting to shopping cart';
|
||||||
|
```
|
||||||
|
|
||||||
|
### res.attachment([filename])
|
||||||
|
|
||||||
|
Set `Content-Disposition` to "attachment" to signal the client
|
||||||
|
to prompt for download. Optionally specify the `filename` of the
|
||||||
|
download.
|
||||||
|
|
||||||
|
### res.headerSent
|
||||||
|
|
||||||
|
Check if a response header has already been sent. Useful for seeing
|
||||||
|
if the client may be notified on error.
|
||||||
|
|
916
lib/context.js
916
lib/context.js
File diff suppressed because it is too large
Load diff
612
lib/request.js
Normal file
612
lib/request.js
Normal file
|
@ -0,0 +1,612 @@
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module dependencies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var debug = require('debug')('koa:context');
|
||||||
|
var Negotiator = require('negotiator');
|
||||||
|
var statuses = require('./status');
|
||||||
|
var Cookies = require('cookies');
|
||||||
|
var qs = require('querystring');
|
||||||
|
var Stream = require('stream');
|
||||||
|
var fresh = require('fresh');
|
||||||
|
var http = require('http');
|
||||||
|
var path = require('path');
|
||||||
|
var mime = require('mime');
|
||||||
|
var basename = path.basename;
|
||||||
|
var extname = path.extname;
|
||||||
|
var url = require('url');
|
||||||
|
var parse = url.parse;
|
||||||
|
var stringify = url.format;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expose `Request`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize a new Request.
|
||||||
|
*
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
function Request(ctx){
|
||||||
|
this.app = ctx.app;
|
||||||
|
this.req = ctx.req;
|
||||||
|
this.ctx = ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prototype.
|
||||||
|
*/
|
||||||
|
|
||||||
|
Request.prototype = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return request header.
|
||||||
|
*
|
||||||
|
* @return {Object}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
get header() {
|
||||||
|
return this.req.headers;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get request URL.
|
||||||
|
*
|
||||||
|
* @return {String}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
get url() {
|
||||||
|
return this.req.url;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set request URL.
|
||||||
|
*
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
set url(val) {
|
||||||
|
this.req.url = val;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get request method.
|
||||||
|
*
|
||||||
|
* @return {String}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
get method() {
|
||||||
|
return this.req.method;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set request method.
|
||||||
|
*
|
||||||
|
* @param {String} val
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
set method(val) {
|
||||||
|
this.req.method = val;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get request pathname.
|
||||||
|
*
|
||||||
|
* @return {String}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
get path() {
|
||||||
|
var c = this._pathcache = this._pathcache || {};
|
||||||
|
return c[this.url] || (c[this.url] = parse(this.url).pathname);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set pathname, retaining the query-string when present.
|
||||||
|
*
|
||||||
|
* @param {String} path
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
set path(path) {
|
||||||
|
var url = parse(this.url);
|
||||||
|
url.pathname = path;
|
||||||
|
this.url = stringify(url);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get parsed query-string.
|
||||||
|
*
|
||||||
|
* @return {Object}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
get query() {
|
||||||
|
var str = this.querystring;
|
||||||
|
if (!str) return {};
|
||||||
|
|
||||||
|
var c = this._querycache = this._querycache || {};
|
||||||
|
return c[str] || (c[str] = qs.parse(str));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set query-string as an object.
|
||||||
|
*
|
||||||
|
* @param {Object} obj
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
set query(obj) {
|
||||||
|
this.querystring = qs.stringify(obj);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get query string.
|
||||||
|
*
|
||||||
|
* @return {String}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
get querystring() {
|
||||||
|
var c = this._qscache = this._qscache || {};
|
||||||
|
return c[this.url] || (c[this.url] = parse(this.url).query || '');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set querystring.
|
||||||
|
*
|
||||||
|
* @param {String} str
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
set querystring(str) {
|
||||||
|
var url = parse(this.url);
|
||||||
|
url.search = str;
|
||||||
|
this.url = stringify(url);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the "Host" header field hostname
|
||||||
|
* and support X-Forwarded-Host when a
|
||||||
|
* proxy is enabled.
|
||||||
|
*
|
||||||
|
* @return {String}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
get host() {
|
||||||
|
var proxy = this.app.proxy;
|
||||||
|
var host = proxy && this.get('X-Forwarded-Host');
|
||||||
|
host = host || this.get('Host');
|
||||||
|
if (!host) return;
|
||||||
|
return host.split(/\s*,\s*/)[0].split(':')[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the request is fresh, aka
|
||||||
|
* Last-Modified and/or the ETag
|
||||||
|
* still match.
|
||||||
|
*
|
||||||
|
* @return {Boolean}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
get fresh() {
|
||||||
|
var method = this.method;
|
||||||
|
var s = this.ctx.status;
|
||||||
|
|
||||||
|
// GET or HEAD for weak freshness validation only
|
||||||
|
if ('GET' != method && 'HEAD' != method) return false;
|
||||||
|
|
||||||
|
// 2xx or 304 as per rfc2616 14.26
|
||||||
|
if ((s >= 200 && s < 300) || 304 == s) {
|
||||||
|
return fresh(this.header, this.ctx.response.header);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the request is stale, aka
|
||||||
|
* "Last-Modified" and / or the "ETag" for the
|
||||||
|
* resource has changed.
|
||||||
|
*
|
||||||
|
* @return {Boolean}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
get stale() {
|
||||||
|
return !this.fresh;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the request is idempotent.
|
||||||
|
*
|
||||||
|
* @return {Boolean}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
get idempotent() {
|
||||||
|
return 'GET' == this.method
|
||||||
|
|| 'HEAD' == this.method;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the request socket.
|
||||||
|
*
|
||||||
|
* @return {Connection}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
get socket() {
|
||||||
|
// TODO: TLS
|
||||||
|
return this.req.socket;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return parsed Content-Length when present.
|
||||||
|
*
|
||||||
|
* @return {Number}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
get length() {
|
||||||
|
var len = this.get('Content-Length');
|
||||||
|
if (null == len) return;
|
||||||
|
return ~~len;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the protocol string "http" or "https"
|
||||||
|
* when requested with TLS. When the proxy setting
|
||||||
|
* is enabled the "X-Forwarded-Proto" header
|
||||||
|
* field will be trusted. If you're running behind
|
||||||
|
* a reverse proxy that supplies https for you this
|
||||||
|
* may be enabled.
|
||||||
|
*
|
||||||
|
* @return {String}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
get protocol() {
|
||||||
|
var proxy = this.app.proxy;
|
||||||
|
if (this.socket.encrypted) return 'https';
|
||||||
|
if (!proxy) return 'http';
|
||||||
|
var proto = this.get('X-Forwarded-Proto') || 'http';
|
||||||
|
return proto.split(/\s*,\s*/)[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Short-hand for:
|
||||||
|
*
|
||||||
|
* this.protocol == 'https'
|
||||||
|
*
|
||||||
|
* @return {Boolean}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
get secure() {
|
||||||
|
return 'https' == this.protocol;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the remote address, or when
|
||||||
|
* `app.proxy` is `true` return
|
||||||
|
* the upstream addr.
|
||||||
|
*
|
||||||
|
* @return {String}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
get ip() {
|
||||||
|
return this.ips[0] || this.connection.remoteAddress;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When `app.proxy` is `true`, parse
|
||||||
|
* the "X-Forwarded-For" ip address list.
|
||||||
|
*
|
||||||
|
* For example if the value were "client, proxy1, proxy2"
|
||||||
|
* you would receive the array `["client", "proxy1", "proxy2"]`
|
||||||
|
* where "proxy2" is the furthest down-stream.
|
||||||
|
*
|
||||||
|
* @return {Array}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
get ips() {
|
||||||
|
var proxy = this.app.proxy;
|
||||||
|
var val = this.get('X-Forwarded-For');
|
||||||
|
return proxy && val
|
||||||
|
? val.split(/ *, */)
|
||||||
|
: [];
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return subdomains as an array.
|
||||||
|
*
|
||||||
|
* Subdomains are the dot-separated parts of the host before the main domain of
|
||||||
|
* the app. By default, the domain of the app is assumed to be the last two
|
||||||
|
* parts of the host. This can be changed by setting `app.subdomainOffset`.
|
||||||
|
*
|
||||||
|
* For example, if the domain is "tobi.ferrets.example.com":
|
||||||
|
* If `app.subdomainOffset` is not set, this.subdomains is `["ferrets", "tobi"]`.
|
||||||
|
* If `app.subdomainOffset` is 3, this.subdomains is `["tobi"]`.
|
||||||
|
*
|
||||||
|
* @return {Array}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
get subdomains() {
|
||||||
|
var offset = this.app.subdomainOffset;
|
||||||
|
return (this.host || '')
|
||||||
|
.split('.')
|
||||||
|
.reverse()
|
||||||
|
.slice(offset);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given `type(s)` is acceptable, returning
|
||||||
|
* the best match when true, otherwise `undefined`, in which
|
||||||
|
* case you should respond with 406 "Not Acceptable".
|
||||||
|
*
|
||||||
|
* The `type` value may be a single mime type string
|
||||||
|
* such as "application/json", the extension name
|
||||||
|
* such as "json" or an array `["json", "html", "text/plain"]`. When a list
|
||||||
|
* or array is given the _best_ match, if any is returned.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
*
|
||||||
|
* // Accept: text/html
|
||||||
|
* this.accepts('html');
|
||||||
|
* // => "html"
|
||||||
|
*
|
||||||
|
* // Accept: text/*, application/json
|
||||||
|
* this.accepts('html');
|
||||||
|
* // => "html"
|
||||||
|
* this.accepts('text/html');
|
||||||
|
* // => "text/html"
|
||||||
|
* this.accepts('json', 'text');
|
||||||
|
* // => "json"
|
||||||
|
* this.accepts('application/json');
|
||||||
|
* // => "application/json"
|
||||||
|
*
|
||||||
|
* // Accept: text/*, application/json
|
||||||
|
* this.accepts('image/png');
|
||||||
|
* this.accepts('png');
|
||||||
|
* // => undefined
|
||||||
|
*
|
||||||
|
* // Accept: text/*;q=.5, application/json
|
||||||
|
* this.accepts(['html', 'json']);
|
||||||
|
* this.accepts('html', 'json');
|
||||||
|
* // => "json"
|
||||||
|
*
|
||||||
|
* @param {String|Array} type(s)...
|
||||||
|
* @return {String|Array|Boolean}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
accepts: function(types){
|
||||||
|
if (!Array.isArray(types)) types = [].slice.call(arguments);
|
||||||
|
var n = new Negotiator(this.req);
|
||||||
|
if (!types.length) return n.preferredMediaTypes();
|
||||||
|
var mimes = types.map(extToMime);
|
||||||
|
var accepts = n.preferredMediaTypes(mimes);
|
||||||
|
var first = accepts[0];
|
||||||
|
if (!first) return false;
|
||||||
|
return types[mimes.indexOf(first)];
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return accepted encodings or best fit based on `encodings`.
|
||||||
|
*
|
||||||
|
* Given `Accept-Encoding: gzip, deflate`
|
||||||
|
* an array sorted by quality is returned:
|
||||||
|
*
|
||||||
|
* ['gzip', 'deflate']
|
||||||
|
*
|
||||||
|
* @param {String|Array} encoding(s)...
|
||||||
|
* @return {String|Array}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
acceptsEncodings: function(encodings) {
|
||||||
|
if (!Array.isArray(encodings)) encodings = [].slice.call(arguments);
|
||||||
|
var n = new Negotiator(this.req);
|
||||||
|
if (!encodings.length) return n.preferredEncodings();
|
||||||
|
return n.preferredEncodings(encodings)[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return accepted charsets or best fit based on `charsets`.
|
||||||
|
*
|
||||||
|
* Given `Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5`
|
||||||
|
* an array sorted by quality is returned:
|
||||||
|
*
|
||||||
|
* ['utf-8', 'utf-7', 'iso-8859-1']
|
||||||
|
*
|
||||||
|
* @param {String|Array} charset(s)...
|
||||||
|
* @return {String|Array}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
acceptsCharsets: function(charsets) {
|
||||||
|
if (!Array.isArray(charsets)) charsets = [].slice.call(arguments);
|
||||||
|
var n = new Negotiator(this.req);
|
||||||
|
if (!charsets.length) return n.preferredCharsets();
|
||||||
|
return n.preferredCharsets(charsets)[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return accepted languages or best fit based on `langs`.
|
||||||
|
*
|
||||||
|
* Given `Accept-Language: en;q=0.8, es, pt`
|
||||||
|
* an array sorted by quality is returned:
|
||||||
|
*
|
||||||
|
* ['es', 'pt', 'en']
|
||||||
|
*
|
||||||
|
* @param {String|Array} lang(s)...
|
||||||
|
* @return {Array|String}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
acceptsLanguages: function(langs) {
|
||||||
|
if (!Array.isArray(langs)) langs = [].slice.call(arguments);
|
||||||
|
var n = new Negotiator(this.req);
|
||||||
|
if (!langs.length) return n.preferredLanguages();
|
||||||
|
return n.preferredLanguages(langs)[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vary on `field`.
|
||||||
|
*
|
||||||
|
* @param {String} field
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
vary: function(field){
|
||||||
|
this.append('Vary', field);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the incoming request contains the "Content-Type"
|
||||||
|
* header field, and it contains the give mime `type`.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
*
|
||||||
|
* // With Content-Type: text/html; charset=utf-8
|
||||||
|
* this.is('html');
|
||||||
|
* this.is('text/html');
|
||||||
|
* this.is('text/*');
|
||||||
|
* // => true
|
||||||
|
*
|
||||||
|
* // When Content-Type is application/json
|
||||||
|
* this.is('json');
|
||||||
|
* this.is('application/json');
|
||||||
|
* this.is('application/*');
|
||||||
|
* // => true
|
||||||
|
*
|
||||||
|
* this.is('html');
|
||||||
|
* // => false
|
||||||
|
*
|
||||||
|
* @param {String} type
|
||||||
|
* @return {Boolean}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
is: function(type){
|
||||||
|
var ct = this.type;
|
||||||
|
if (!ct) return false;
|
||||||
|
ct = ct.split(';')[0];
|
||||||
|
|
||||||
|
// extension given
|
||||||
|
if (!~type.indexOf('/')) type = mime.lookup(type);
|
||||||
|
|
||||||
|
// type or subtype match
|
||||||
|
if (~type.indexOf('*')) {
|
||||||
|
type = type.split('/');
|
||||||
|
ct = ct.split('/');
|
||||||
|
if ('*' == type[0] && type[1] == ct[1]) return true;
|
||||||
|
if ('*' == type[1] && type[0] == ct[0]) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// exact match
|
||||||
|
return type == ct;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the request mime type void of
|
||||||
|
* parameters such as "charset".
|
||||||
|
*
|
||||||
|
* @return {String}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
get type() {
|
||||||
|
var type = this.get('Content-Type');
|
||||||
|
if (!type) return;
|
||||||
|
return type.split(';')[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return request header.
|
||||||
|
*
|
||||||
|
* The `Referrer` header field is special-cased,
|
||||||
|
* both `Referrer` and `Referer` are interchangeable.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
*
|
||||||
|
* this.get('Content-Type');
|
||||||
|
* // => "text/plain"
|
||||||
|
*
|
||||||
|
* this.get('content-type');
|
||||||
|
* // => "text/plain"
|
||||||
|
*
|
||||||
|
* this.get('Something');
|
||||||
|
* // => undefined
|
||||||
|
*
|
||||||
|
* @param {String} name
|
||||||
|
* @return {String}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
get: function(name){
|
||||||
|
var req = this.req;
|
||||||
|
switch (name = name.toLowerCase()) {
|
||||||
|
case 'referer':
|
||||||
|
case 'referrer':
|
||||||
|
return req.headers.referrer || req.headers.referer;
|
||||||
|
default:
|
||||||
|
return req.headers[name];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inspect implementation.
|
||||||
|
*
|
||||||
|
* TODO: add tests
|
||||||
|
*
|
||||||
|
* @return {Object}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
inspect: function(){
|
||||||
|
return this.toJSON();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return JSON representation.
|
||||||
|
*
|
||||||
|
* @return {Object}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
toJSON: function(){
|
||||||
|
return {
|
||||||
|
method: this.method,
|
||||||
|
header: this.header
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert extnames to mime.
|
||||||
|
*
|
||||||
|
* @param {String} type
|
||||||
|
* @return {String}
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
function extToMime(type) {
|
||||||
|
if (~type.indexOf('/')) return type;
|
||||||
|
return mime.lookup(type);
|
||||||
|
}
|
433
lib/response.js
Normal file
433
lib/response.js
Normal file
|
@ -0,0 +1,433 @@
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module dependencies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var debug = require('debug')('koa:response');
|
||||||
|
var Negotiator = require('negotiator');
|
||||||
|
var statuses = require('./status');
|
||||||
|
var Cookies = require('cookies');
|
||||||
|
var qs = require('querystring');
|
||||||
|
var Stream = require('stream');
|
||||||
|
var fresh = require('fresh');
|
||||||
|
var http = require('http');
|
||||||
|
var path = require('path');
|
||||||
|
var mime = require('mime');
|
||||||
|
var basename = path.basename;
|
||||||
|
var extname = path.extname;
|
||||||
|
var url = require('url');
|
||||||
|
var parse = url.parse;
|
||||||
|
var stringify = url.format;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expose `Response`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize a new Response.
|
||||||
|
*
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
function Response(ctx){
|
||||||
|
this.res = ctx.res;
|
||||||
|
this.ctx = ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prototype.
|
||||||
|
*/
|
||||||
|
|
||||||
|
Response.prototype = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return response header.
|
||||||
|
*
|
||||||
|
* @return {Object}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
get header() {
|
||||||
|
// TODO: wtf
|
||||||
|
return this.res._headers || {};
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return response status string.
|
||||||
|
*
|
||||||
|
* @return {String}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
get statusString() {
|
||||||
|
return http.STATUS_CODES[this.status];
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get response status code.
|
||||||
|
*
|
||||||
|
* @return {Number}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
get status() {
|
||||||
|
return this.res.statusCode;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set response status code.
|
||||||
|
*
|
||||||
|
* @param {Number|String} val
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
set status(val) {
|
||||||
|
if ('string' == typeof val) {
|
||||||
|
var n = statuses[val.toLowerCase()];
|
||||||
|
if (!n) throw new Error(statusError(val));
|
||||||
|
val = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.res.statusCode = val;
|
||||||
|
|
||||||
|
var noContent = 304 == this.status || 204 == this.status;
|
||||||
|
if (noContent && this.body) this.body = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get response body.
|
||||||
|
*
|
||||||
|
* @return {Mixed}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
get body() {
|
||||||
|
return this._body;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set response body.
|
||||||
|
*
|
||||||
|
* @param {String|Buffer|Object|Stream} val
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
set body(val) {
|
||||||
|
this._body = val;
|
||||||
|
|
||||||
|
// no content
|
||||||
|
if (null == val) {
|
||||||
|
var s = this.status;
|
||||||
|
this.status = 304 == s ? 304 : 204;
|
||||||
|
this.res.removeHeader('Content-Type');
|
||||||
|
this.res.removeHeader('Content-Length');
|
||||||
|
this.res.removeHeader('Transfer-Encoding');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the content-type only if not yet set
|
||||||
|
var setType = !this.header['content-type'];
|
||||||
|
|
||||||
|
// string
|
||||||
|
if ('string' == typeof val) {
|
||||||
|
if (setType) this.type = ~val.indexOf('<') ? 'html' : 'text';
|
||||||
|
this.length = Buffer.byteLength(val);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// buffer
|
||||||
|
if (Buffer.isBuffer(val)) {
|
||||||
|
if (setType) this.type = 'bin';
|
||||||
|
this.length = val.length;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// stream
|
||||||
|
if (val instanceof Stream) {
|
||||||
|
if (setType) this.type = 'bin';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// json
|
||||||
|
this.type = 'json';
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set Content-Length field to `n`.
|
||||||
|
*
|
||||||
|
* @param {Number} n
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
set length(n) {
|
||||||
|
this.set('Content-Length', n);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return parsed response Content-Length when present.
|
||||||
|
*
|
||||||
|
* @return {Number}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
get length() {
|
||||||
|
var len = this.header['content-length'];
|
||||||
|
var body = this.body;
|
||||||
|
|
||||||
|
if (null == len) {
|
||||||
|
if (!body) return;
|
||||||
|
if ('string' == typeof body) return Buffer.byteLength(body);
|
||||||
|
return body.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ~~len;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a header has been written to the socket.
|
||||||
|
*
|
||||||
|
* @return {Boolean}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
get headerSent() {
|
||||||
|
return this.res.headersSent;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vary on `field`.
|
||||||
|
*
|
||||||
|
* @param {String} field
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
vary: function(field){
|
||||||
|
this.append('Vary', field);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a 302 redirect to `url`.
|
||||||
|
*
|
||||||
|
* The string "back" is special-cased
|
||||||
|
* to provide Referrer support, when Referrer
|
||||||
|
* is not present `alt` or "/" is used.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
*
|
||||||
|
* this.redirect('back');
|
||||||
|
* this.redirect('back', '/index.html');
|
||||||
|
* this.redirect('/login');
|
||||||
|
* this.redirect('http://google.com');
|
||||||
|
*
|
||||||
|
* @param {String} url
|
||||||
|
* @param {String} alt
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
redirect: function(url, alt){
|
||||||
|
if ('back' == url) url = this.ctx.get('Referrer') || alt || '/';
|
||||||
|
this.set('Location', url);
|
||||||
|
if (!~[300, 301, 302, 303, 305, 307].indexOf(this.status)) this.status = 302;
|
||||||
|
|
||||||
|
// html
|
||||||
|
if (this.ctx.accepts('html')) {
|
||||||
|
url = escape(url);
|
||||||
|
this.type = 'text/html; charset=utf-8';
|
||||||
|
this.body = 'Redirecting to <a href="' + url + '">' + url + '</a>.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// text
|
||||||
|
this.body = 'Redirecting to ' + url + '.';
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set Content-Disposition header to "attachment" with optional `filename`.
|
||||||
|
*
|
||||||
|
* @param {String} filename
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
attachment: function(filename){
|
||||||
|
if (filename) this.type = extname(filename);
|
||||||
|
this.set('Content-Disposition', filename
|
||||||
|
? 'attachment; filename="' + basename(filename) + '"'
|
||||||
|
: 'attachment');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set Content-Type response header with `type` through `mime.lookup()`
|
||||||
|
* when it does not contain "/", or set the Content-Type to `type` otherwise.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
*
|
||||||
|
* this.type = '.html';
|
||||||
|
* this.type = 'html';
|
||||||
|
* this.type = 'json';
|
||||||
|
* this.type = 'application/json';
|
||||||
|
* this.type = 'png';
|
||||||
|
*
|
||||||
|
* @param {String} type
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
set type(type){
|
||||||
|
if (!~type.indexOf('/')) {
|
||||||
|
type = mime.lookup(type);
|
||||||
|
var cs = mime.charsets.lookup(type);
|
||||||
|
if (cs) type += '; charset=' + cs.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.set('Content-Type', type);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the request mime type void of
|
||||||
|
* parameters such as "charset".
|
||||||
|
*
|
||||||
|
* @return {String}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
get type() {
|
||||||
|
var type = this.get('Content-Type');
|
||||||
|
if (!type) return;
|
||||||
|
return type.split(';')[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return response header.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
*
|
||||||
|
* this.get('Content-Type');
|
||||||
|
* // => "text/plain"
|
||||||
|
*
|
||||||
|
* this.get('content-type');
|
||||||
|
* // => "text/plain"
|
||||||
|
*
|
||||||
|
* @param {String} name
|
||||||
|
* @return {String}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
get: function(name){
|
||||||
|
return this.header[name.toLowerCase()];
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set header `field` to `val`, or pass
|
||||||
|
* an object of header fields.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
*
|
||||||
|
* this.set('Foo', ['bar', 'baz']);
|
||||||
|
* this.set('Accept', 'application/json');
|
||||||
|
* this.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });
|
||||||
|
*
|
||||||
|
* @param {String|Object|Array} field
|
||||||
|
* @param {String} val
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
set: function(field, val){
|
||||||
|
if (2 == arguments.length) {
|
||||||
|
if (Array.isArray(val)) val = val.map(String);
|
||||||
|
else val = String(val);
|
||||||
|
this.res.setHeader(field, val);
|
||||||
|
} else {
|
||||||
|
for (var key in field) {
|
||||||
|
this.set(key, field[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append `val` to header `field`.
|
||||||
|
*
|
||||||
|
* @param {String} field
|
||||||
|
* @param {String} val
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
append: function(field, val){
|
||||||
|
field = field.toLowerCase();
|
||||||
|
var header = this.header;
|
||||||
|
var list = header[field];
|
||||||
|
|
||||||
|
// not set
|
||||||
|
if (!list) return this.set(field, val);
|
||||||
|
|
||||||
|
// append
|
||||||
|
list = list.split(/ *, */);
|
||||||
|
if (!~list.indexOf(val)) list.push(val);
|
||||||
|
this.set(field, list.join(', '));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inspect implementation.
|
||||||
|
*
|
||||||
|
* TODO: add tests
|
||||||
|
*
|
||||||
|
* @return {Object}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
inspect: function(){
|
||||||
|
var o = this.toJSON();
|
||||||
|
o.body = this.body;
|
||||||
|
o.statusString = this.statusString;
|
||||||
|
return o;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return JSON representation.
|
||||||
|
*
|
||||||
|
* @return {Object}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
toJSON: function(){
|
||||||
|
return {
|
||||||
|
status: this.status,
|
||||||
|
header: this.header
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return status error message.
|
||||||
|
*
|
||||||
|
* @param {String} val
|
||||||
|
* @return {String}
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
function statusError(val) {
|
||||||
|
var s = 'invalid status string "' + val + '", try:\n\n';
|
||||||
|
|
||||||
|
Object.keys(statuses).forEach(function(name){
|
||||||
|
var n = statuses[name];
|
||||||
|
s += ' - ' + n + ' "' + name + '"\n';
|
||||||
|
});
|
||||||
|
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape special characters in the given string of html.
|
||||||
|
*
|
||||||
|
* @param {String} html
|
||||||
|
* @return {String}
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
function escape(html) {
|
||||||
|
return String(html)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>');
|
||||||
|
}
|
870
test/context.js
870
test/context.js
|
@ -13,872 +13,4 @@ function context(req, res) {
|
||||||
return ctx;
|
return ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('ctx.body=', function(){
|
module.exports = context;
|
||||||
describe('when Content-Type is set', function(){
|
|
||||||
it('should not override', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.type = 'png';
|
|
||||||
ctx.body = new Buffer('something');
|
|
||||||
assert('image/png' == ctx.responseHeader['content-type']);
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when body is an object', function(){
|
|
||||||
it('should override as json', function(){
|
|
||||||
var ctx = context();
|
|
||||||
|
|
||||||
ctx.body = '<em>hey</em>';
|
|
||||||
assert('text/html; charset=utf-8' == ctx.responseHeader['content-type']);
|
|
||||||
|
|
||||||
ctx.body = { foo: 'bar' };
|
|
||||||
assert('application/json' == ctx.responseHeader['content-type']);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should override length', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.type = 'html';
|
|
||||||
ctx.body = 'something';
|
|
||||||
ctx.responseLength.should.equal(9);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when a string is given', function(){
|
|
||||||
it('should default to text', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.body = 'Tobi';
|
|
||||||
assert('text/plain; charset=utf-8' == ctx.responseHeader['content-type']);
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should set length', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.body = 'Tobi';
|
|
||||||
assert('4' == ctx.responseHeader['content-length']);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when an html string is given', function(){
|
|
||||||
it('should default to html', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.body = '<h1>Tobi</h1>';
|
|
||||||
assert('text/html; charset=utf-8' == ctx.responseHeader['content-type']);
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should set length', function(){
|
|
||||||
var string = '<h1>Tobi</h1>';
|
|
||||||
var ctx = context();
|
|
||||||
ctx.body = string;
|
|
||||||
assert.equal(ctx.responseLength, Buffer.byteLength(string));
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should set length when body is overriden', function(){
|
|
||||||
var string = '<h1>Tobi</h1>';
|
|
||||||
var ctx = context();
|
|
||||||
ctx.body = string;
|
|
||||||
ctx.body = string + string;
|
|
||||||
assert.equal(ctx.responseLength, 2 * Buffer.byteLength(string));
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when a stream is given', function(){
|
|
||||||
it('should default to an octet stream', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.body = fs.createReadStream('LICENSE');
|
|
||||||
assert('application/octet-stream' == ctx.responseHeader['content-type']);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when a buffer is given', function(){
|
|
||||||
it('should default to an octet stream', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.body = new Buffer('hey');
|
|
||||||
assert('application/octet-stream' == ctx.responseHeader['content-type']);
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should set length', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.body = new Buffer('Tobi');
|
|
||||||
assert('4' == ctx.responseHeader['content-length']);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when an object is given', function(){
|
|
||||||
it('should default to json', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.body = { foo: 'bar' };
|
|
||||||
assert('application/json' == ctx.responseHeader['content-type']);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ctx.error(msg)', function(){
|
|
||||||
it('should set .status to 500', function(done){
|
|
||||||
var ctx = context();
|
|
||||||
|
|
||||||
try {
|
|
||||||
ctx.error('boom');
|
|
||||||
} catch (err) {
|
|
||||||
assert(500 == err.status);
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ctx.error(msg, status)', function(){
|
|
||||||
it('should throw an error', function(done){
|
|
||||||
var ctx = context();
|
|
||||||
|
|
||||||
try {
|
|
||||||
ctx.error('name required', 400);
|
|
||||||
} catch (err) {
|
|
||||||
assert('name required' == err.message);
|
|
||||||
assert(400 == err.status);
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ctx.error(status)', function(){
|
|
||||||
it('should throw an error', function(done){
|
|
||||||
var ctx = context();
|
|
||||||
|
|
||||||
try {
|
|
||||||
ctx.error(400);
|
|
||||||
} catch (err) {
|
|
||||||
assert('Bad Request' == err.message);
|
|
||||||
assert(400 == err.status);
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ctx.length', function(){
|
|
||||||
describe('when Content-Length is defined', function(){
|
|
||||||
it('should return a number', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.header['content-length'] = '120';
|
|
||||||
ctx.length.should.equal(120);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ctx.responseLength', function(){
|
|
||||||
describe('when Content-Length is defined', function(){
|
|
||||||
it('should return a number', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.set('Content-Length', '1024');
|
|
||||||
ctx.responseLength.should.equal(1024);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when Content-Length is not defined', function(){
|
|
||||||
describe('and a .body is set', function(){
|
|
||||||
it('should return a number', function(){
|
|
||||||
var ctx = context();
|
|
||||||
|
|
||||||
ctx.body = 'foo';
|
|
||||||
ctx.responseLength.should.equal(3);
|
|
||||||
|
|
||||||
ctx.body = new Buffer('foo');
|
|
||||||
ctx.responseLength.should.equal(3);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('and .body is not', function(){
|
|
||||||
it('should return undefined', function(){
|
|
||||||
var ctx = context();
|
|
||||||
assert(null == ctx.responseLength);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ctx.header', function(){
|
|
||||||
it('should return the request header object', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.header.should.equal(ctx.req.headers);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ctx.protocol', function(){
|
|
||||||
describe('when encrypted', function(){
|
|
||||||
it('should return "https"', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.req.socket = { encrypted: true };
|
|
||||||
ctx.protocol.should.equal('https');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when unencrypted', function(){
|
|
||||||
it('should return "http"', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.req.socket = {};
|
|
||||||
ctx.protocol.should.equal('http');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when X-Forwarded-Proto is set', function(){
|
|
||||||
describe('and proxy is trusted', function(){
|
|
||||||
it('should be used', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.app.proxy = true;
|
|
||||||
ctx.req.socket = {};
|
|
||||||
ctx.header['x-forwarded-proto'] = 'https, http';
|
|
||||||
ctx.protocol.should.equal('https');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('and proxy is not trusted', function(){
|
|
||||||
it('should not be used', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.req.socket = {};
|
|
||||||
ctx.header['x-forwarded-proto'] = 'https, http';
|
|
||||||
ctx.protocol.should.equal('http');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ctx.secure', function(){
|
|
||||||
it('should return true when encrypted', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.req.socket = { encrypted: true };
|
|
||||||
ctx.secure.should.be.true;
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ctx.host', function(){
|
|
||||||
it('should return host void of port', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.header.host = 'foo.com:3000';
|
|
||||||
ctx.host.should.equal('foo.com');
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when X-Forwarded-Host is present', function(){
|
|
||||||
describe('and proxy is not trusted', function(){
|
|
||||||
it('should be ignored', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.header['x-forwarded-host'] = 'bar.com';
|
|
||||||
ctx.header['host'] = 'foo.com';
|
|
||||||
ctx.host.should.equal('foo.com')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('and proxy is trusted', function(){
|
|
||||||
it('should be used', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.app.proxy = true;
|
|
||||||
ctx.header['x-forwarded-host'] = 'bar.com, baz.com';
|
|
||||||
ctx.header['host'] = 'foo.com';
|
|
||||||
ctx.host.should.equal('bar.com')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ctx.status=', function(){
|
|
||||||
describe('when a status string', function(){
|
|
||||||
describe('and valid', function(){
|
|
||||||
it('should set the status', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.status = 'forbidden';
|
|
||||||
ctx.status.should.equal(403);
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should be case-insensitive', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.status = 'ForBidden';
|
|
||||||
ctx.status.should.equal(403);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('and invalid', function(){
|
|
||||||
it('should throw', function(){
|
|
||||||
var ctx = context();
|
|
||||||
var err;
|
|
||||||
|
|
||||||
try {
|
|
||||||
ctx.status = 'maru';
|
|
||||||
} catch (e) {
|
|
||||||
err = e;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(err);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
function strip(status) {
|
|
||||||
it('should strip content related header fields', function(done){
|
|
||||||
var app = koa();
|
|
||||||
|
|
||||||
app.use(function(next){
|
|
||||||
return function *(){
|
|
||||||
this.body = { foo: 'bar' };
|
|
||||||
this.set('Content-Type', 'application/json');
|
|
||||||
this.set('Content-Length', '15');
|
|
||||||
this.set('Transfer-Encoding', 'chunked');
|
|
||||||
this.status = status;
|
|
||||||
assert(null == this.responseHeader['content-type']);
|
|
||||||
assert(null == this.responseHeader['content-length']);
|
|
||||||
assert(null == this.responseHeader['transfer-encoding']);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
request(app.listen())
|
|
||||||
.get('/')
|
|
||||||
.expect(status)
|
|
||||||
.end(done);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('when 204', function(){
|
|
||||||
strip(204);
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when 304', function(){
|
|
||||||
strip(304);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ctx.stale', function(){
|
|
||||||
it('should be the inverse of ctx.fresh', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.status = 200;
|
|
||||||
ctx.req.method = 'GET';
|
|
||||||
ctx.req.headers['if-none-match'] = '123';
|
|
||||||
ctx.res._headers['etag'] = '123';
|
|
||||||
ctx.fresh.should.be.true;
|
|
||||||
ctx.stale.should.be.false;
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ctx.fresh', function(){
|
|
||||||
describe('the response is non-2xx', function(){
|
|
||||||
it('should return false', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.status = 404;
|
|
||||||
ctx.req.method = 'GET';
|
|
||||||
ctx.req.headers['if-none-match'] = '123';
|
|
||||||
ctx.res._headers['etag'] = '123';
|
|
||||||
ctx.fresh.should.be.false;
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('the response is 2xx', function(){
|
|
||||||
describe('and etag matches', function(){
|
|
||||||
it('should return true', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.status = 200;
|
|
||||||
ctx.req.method = 'GET';
|
|
||||||
ctx.req.headers['if-none-match'] = '123';
|
|
||||||
ctx.res._headers['etag'] = '123';
|
|
||||||
ctx.fresh.should.be.true;
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('and etag do not match', function(){
|
|
||||||
it('should return false', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.status = 200;
|
|
||||||
ctx.req.method = 'GET';
|
|
||||||
ctx.req.headers['if-none-match'] = '123';
|
|
||||||
ctx.res._headers['etag'] = 'hey';
|
|
||||||
ctx.fresh.should.be.false;
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ctx.vary(field)', function(){
|
|
||||||
describe('when Vary is not set', function(){
|
|
||||||
it('should set it', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.vary('Accept');
|
|
||||||
ctx.responseHeader.vary.should.equal('Accept');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when Vary is set', function(){
|
|
||||||
it('should append', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.vary('Accept');
|
|
||||||
ctx.vary('Accept-Encoding');
|
|
||||||
ctx.responseHeader.vary.should.equal('Accept, Accept-Encoding');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when Vary already contains the value', function(){
|
|
||||||
it('should not append', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.vary('Accept');
|
|
||||||
ctx.vary('Accept-Encoding');
|
|
||||||
ctx.vary('Accept');
|
|
||||||
ctx.vary('Accept-Encoding');
|
|
||||||
ctx.responseHeader.vary.should.equal('Accept, Accept-Encoding');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ctx.accepts(types)', function(){
|
|
||||||
describe('with no arguments', function(){
|
|
||||||
it('should return all accepted types', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.req.headers.accept = 'application/*;q=0.2, image/jpeg;q=0.8, text/html, text/plain';
|
|
||||||
ctx.accepts().should.eql(['text/html', 'text/plain', 'image/jpeg', 'application/*']);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with no valid types', function(){
|
|
||||||
it('should return false', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.accepts('', 'hey').should.be.false;
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when extensions are given', function(){
|
|
||||||
it('should convert to mime types', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.req.headers.accept = 'text/plain, text/html';
|
|
||||||
ctx.accepts('html').should.equal('html');
|
|
||||||
ctx.accepts('.html').should.equal('.html');
|
|
||||||
ctx.accepts('txt').should.equal('txt');
|
|
||||||
ctx.accepts('.txt').should.equal('.txt');
|
|
||||||
ctx.accepts('png').should.be.false;
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when an array is given', function(){
|
|
||||||
it('should return the first match', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.req.headers.accept = 'text/plain, text/html';
|
|
||||||
ctx.accepts(['png', 'text', 'html']).should.equal('text');
|
|
||||||
ctx.accepts(['png', 'html']).should.equal('html');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when multiple arguments are given', function(){
|
|
||||||
it('should return the first match', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.req.headers.accept = 'text/plain, text/html';
|
|
||||||
ctx.accepts('png', 'text', 'html').should.equal('text');
|
|
||||||
ctx.accepts('png', 'html').should.equal('html');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when present in Accept as an exact match', function(){
|
|
||||||
it('should return the type', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.req.headers.accept = 'text/plain, text/html';
|
|
||||||
ctx.accepts('text/html').should.equal('text/html');
|
|
||||||
ctx.accepts('text/plain').should.equal('text/plain');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when present in Accept as a type match', function(){
|
|
||||||
it('should return the type', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.req.headers.accept = 'application/json, */*';
|
|
||||||
ctx.accepts('text/html').should.equal('text/html');
|
|
||||||
ctx.accepts('text/plain').should.equal('text/plain');
|
|
||||||
ctx.accepts('image/png').should.equal('image/png');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when present in Accept as a subtype match', function(){
|
|
||||||
it('should return the type', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.req.headers.accept = 'application/json, text/*';
|
|
||||||
ctx.accepts('text/html').should.equal('text/html');
|
|
||||||
ctx.accepts('text/plain').should.equal('text/plain');
|
|
||||||
ctx.accepts('image/png').should.be.false;
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ctx.acceptsLanguages(langs)', function(){
|
|
||||||
describe('with no arguments', function(){
|
|
||||||
describe('when Accept-Language is populated', function(){
|
|
||||||
it('should return accepted types', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.req.headers['accept-language'] = 'en;q=0.8, es, pt';
|
|
||||||
ctx.acceptsLanguages().should.eql(['es', 'pt', 'en']);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when Accept-Language is not populated', function(){
|
|
||||||
it('should return an empty array', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.acceptsLanguages().should.eql([]);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with multiple arguments', function(){
|
|
||||||
it('should return the best fit', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.req.headers['accept-language'] = 'en;q=0.8, es, pt';
|
|
||||||
ctx.acceptsLanguages('es', 'en').should.equal('es');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with an array', function(){
|
|
||||||
it('should return the best fit', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.req.headers['accept-language'] = 'en;q=0.8, es, pt';
|
|
||||||
ctx.acceptsLanguages(['es', 'en']).should.equal('es');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ctx.acceptsCharsts()', function(){
|
|
||||||
describe('with no arguments', function(){
|
|
||||||
describe('when Accept-Charset is populated', function(){
|
|
||||||
it('should return accepted types', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.req.headers['accept-charset'] = 'utf-8, iso-8859-1;q=0.2, utf-7;q=0.5';
|
|
||||||
ctx.acceptsCharsets().should.eql(['utf-8', 'utf-7', 'iso-8859-1']);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when Accept-Charset is not populated', function(){
|
|
||||||
it('should return an empty array', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.acceptsCharsets().should.eql([]);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with multiple arguments', function(){
|
|
||||||
it('should return the best fit', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.req.headers['accept-charset'] = 'utf-8, iso-8859-1;q=0.2, utf-7;q=0.5';
|
|
||||||
ctx.acceptsCharsets('utf-7', 'utf-8').should.equal('utf-8');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with an array', function(){
|
|
||||||
it('should return the best fit', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.req.headers['accept-charset'] = 'utf-8, iso-8859-1;q=0.2, utf-7;q=0.5';
|
|
||||||
ctx.acceptsCharsets(['utf-7', 'utf-8']).should.equal('utf-8');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ctx.acceptsEncodings()', function(){
|
|
||||||
describe('with no arguments', function(){
|
|
||||||
describe('when Accept-Encoding is populated', function(){
|
|
||||||
it('should return accepted types', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.req.headers['accept-encoding'] = 'gzip, compress;q=0.2';
|
|
||||||
ctx.acceptsEncodings().should.eql(['gzip', 'compress', 'identity']);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when Accept-Encoding is not populated', function(){
|
|
||||||
it('should return identity', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.acceptsEncodings().should.eql(['identity']);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with multiple arguments', function(){
|
|
||||||
it('should return the best fit', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.req.headers['accept-encoding'] = 'gzip, compress;q=0.2';
|
|
||||||
ctx.acceptsEncodings('compress', 'gzip').should.eql('gzip');
|
|
||||||
ctx.acceptsEncodings('gzip', 'compress').should.eql('gzip');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with an array', function(){
|
|
||||||
it('should return the best fit', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.req.headers['accept-encoding'] = 'gzip, compress;q=0.2';
|
|
||||||
ctx.acceptsEncodings(['compress', 'gzip']).should.eql('gzip');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ctx.path', function(){
|
|
||||||
it('should return the pathname', function(){
|
|
||||||
var ctx = new Context(null, {
|
|
||||||
url: '/login?next=/dashboard'
|
|
||||||
});
|
|
||||||
|
|
||||||
ctx.path.should.equal('/login');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ctx.path=', function(){
|
|
||||||
it('should set the pathname', function(){
|
|
||||||
var ctx = new Context(null, {
|
|
||||||
url: '/login?next=/dashboard'
|
|
||||||
});
|
|
||||||
|
|
||||||
ctx.path = '/logout';
|
|
||||||
ctx.path.should.equal('/logout');
|
|
||||||
ctx.url.should.equal('/logout?next=/dashboard');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ctx.get(name)', function(){
|
|
||||||
it('should return the field value', function(){
|
|
||||||
var req = { headers: { 'host': 'http://google.com' } };
|
|
||||||
var ctx = new Context(null, req);
|
|
||||||
ctx.get('HOST').should.equal('http://google.com');
|
|
||||||
ctx.get('Host').should.equal('http://google.com');
|
|
||||||
ctx.get('host').should.equal('http://google.com');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ctx.set(name, val)', function(){
|
|
||||||
it('should set a field value', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.set('x-foo', 'bar');
|
|
||||||
ctx.responseHeader['x-foo'].should.equal('bar');
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should coerce to a string', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.set('x-foo', 5);
|
|
||||||
ctx.responseHeader['x-foo'].should.equal('5');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ctx.set(object)', function(){
|
|
||||||
it('should set multiple fields', function(){
|
|
||||||
var ctx = context();
|
|
||||||
|
|
||||||
ctx.set({
|
|
||||||
foo: '1',
|
|
||||||
bar: '2'
|
|
||||||
});
|
|
||||||
|
|
||||||
ctx.responseHeader.foo.should.equal('1');
|
|
||||||
ctx.responseHeader.bar.should.equal('2');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ctx.query', function(){
|
|
||||||
describe('when missing', function(){
|
|
||||||
it('should return an empty object', function(){
|
|
||||||
var ctx = context({ url: '/' });
|
|
||||||
ctx.query.should.eql({});
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return a parsed query-string', function(){
|
|
||||||
var ctx = context({ url: '/?page=2' });
|
|
||||||
ctx.query.page.should.equal('2');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ctx.query=', function(){
|
|
||||||
it('should stringify and replace the querystring', function(){
|
|
||||||
var ctx = context({ url: '/store/shoes' });
|
|
||||||
ctx.query = { page: 2, color: 'blue' };
|
|
||||||
ctx.url.should.equal('/store/shoes?page=2&color=blue');
|
|
||||||
ctx.querystring.should.equal('page=2&color=blue');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ctx.querystring=', function(){
|
|
||||||
it('should replace the querystring', function(){
|
|
||||||
var ctx = context({ url: '/store/shoes' });
|
|
||||||
ctx.querystring = 'page=2&color=blue';
|
|
||||||
ctx.url.should.equal('/store/shoes?page=2&color=blue');
|
|
||||||
ctx.querystring.should.equal('page=2&color=blue');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ctx.type=', function(){
|
|
||||||
describe('with a mime', function(){
|
|
||||||
it('should set the Content-Type', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.type = 'text/plain';
|
|
||||||
ctx.responseHeader['content-type'].should.equal('text/plain');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with an extension', function(){
|
|
||||||
it('should lookup the mime', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.type = 'json';
|
|
||||||
ctx.responseHeader['content-type'].should.equal('application/json');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ctx.type', function(){
|
|
||||||
describe('with no Content-Type', function(){
|
|
||||||
it('should return null', function(){
|
|
||||||
var ctx = context();
|
|
||||||
assert(null == ctx.type);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with a Content-Type', function(){
|
|
||||||
it('should return the mime', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.req.headers['content-type'] = 'text/html; charset=utf8';
|
|
||||||
ctx.type.should.equal('text/html');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ctx.is(type)', function(){
|
|
||||||
it('should ignore params', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.header['content-type'] = 'text/html; charset=utf-8';
|
|
||||||
ctx.is('text/*').should.be.true;
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('given a mime', function(){
|
|
||||||
it('should check the type', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.header['content-type'] = 'image/png';
|
|
||||||
|
|
||||||
ctx.is('image/png').should.be.true;
|
|
||||||
ctx.is('image/*').should.be.true;
|
|
||||||
ctx.is('*/png').should.be.true;
|
|
||||||
|
|
||||||
ctx.is('image/jpeg').should.be.false;
|
|
||||||
ctx.is('text/*').should.be.false;
|
|
||||||
ctx.is('*/jpeg').should.be.false;
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('given an extension', function(){
|
|
||||||
it('should check the type', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.header['content-type'] = 'image/png';
|
|
||||||
|
|
||||||
ctx.is('png').should.be.true;
|
|
||||||
ctx.is('.png').should.be.true;
|
|
||||||
|
|
||||||
ctx.is('jpeg').should.be.false;
|
|
||||||
ctx.is('.jpeg').should.be.false;
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ctx.attachment([filename])', function(){
|
|
||||||
describe('when given a filename', function(){
|
|
||||||
it('should set the filename param', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.attachment('path/to/tobi.png');
|
|
||||||
var str = 'attachment; filename="tobi.png"';
|
|
||||||
ctx.responseHeader['content-disposition'].should.equal(str);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when omitting filename', function(){
|
|
||||||
it('should not set filename param', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.attachment();
|
|
||||||
ctx.responseHeader['content-disposition'].should.equal('attachment');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ctx.redirect(url)', function(){
|
|
||||||
it('should redirect to the given url', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.redirect('http://google.com');
|
|
||||||
ctx.responseHeader.location.should.equal('http://google.com');
|
|
||||||
ctx.status.should.equal(302);
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with "back"', function(){
|
|
||||||
it('should redirect to Referrer', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.req.headers.referrer = '/login';
|
|
||||||
ctx.redirect('back');
|
|
||||||
ctx.responseHeader.location.should.equal('/login');
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should redirect to Referer', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.req.headers.referer = '/login';
|
|
||||||
ctx.redirect('back');
|
|
||||||
ctx.responseHeader.location.should.equal('/login');
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should default to alt', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.redirect('back', '/index.html');
|
|
||||||
ctx.responseHeader.location.should.equal('/index.html');
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should default redirect to /', function(){
|
|
||||||
var ctx = context();
|
|
||||||
ctx.redirect('back');
|
|
||||||
ctx.responseHeader.location.should.equal('/');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when html is accepted', function(){
|
|
||||||
it('should respond with html', function(){
|
|
||||||
var ctx = context();
|
|
||||||
var url = 'http://google.com';
|
|
||||||
ctx.header.accept = 'text/html';
|
|
||||||
ctx.redirect(url);
|
|
||||||
ctx.responseHeader['content-type'].should.equal('text/html; charset=utf-8');
|
|
||||||
ctx.body.should.equal('Redirecting to <a href="' + url + '">' + url + '</a>.');
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should escape the url', function(){
|
|
||||||
var ctx = context();
|
|
||||||
var url = '<script>';
|
|
||||||
ctx.header.accept = 'text/html';
|
|
||||||
ctx.redirect(url);
|
|
||||||
url = escape(url);
|
|
||||||
ctx.responseHeader['content-type'].should.equal('text/html; charset=utf-8');
|
|
||||||
ctx.body.should.equal('Redirecting to <a href="' + url + '">' + url + '</a>.');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when text is accepted', function(){
|
|
||||||
it('should respond with text', function(){
|
|
||||||
var ctx = context();
|
|
||||||
var url = 'http://google.com';
|
|
||||||
ctx.header.accept = 'text/plain';
|
|
||||||
ctx.redirect(url);
|
|
||||||
ctx.body.should.equal('Redirecting to ' + url + '.');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when status is 301', function(){
|
|
||||||
it('should not change the status code', function(){
|
|
||||||
var ctx = context();
|
|
||||||
var url = 'http://google.com';
|
|
||||||
ctx.status = 301;
|
|
||||||
ctx.header.accept = 'text/plain';
|
|
||||||
ctx.redirect('http://google.com');
|
|
||||||
ctx.status.should.equal(301);
|
|
||||||
ctx.body.should.equal('Redirecting to ' + url + '.');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when status is 304', function(){
|
|
||||||
it('should change the status code', function(){
|
|
||||||
var ctx = context();
|
|
||||||
var url = 'http://google.com';
|
|
||||||
ctx.status = 304;
|
|
||||||
ctx.header.accept = 'text/plain';
|
|
||||||
ctx.redirect('http://google.com');
|
|
||||||
ctx.status.should.equal(302);
|
|
||||||
ctx.body.should.equal('Redirecting to ' + url + '.');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
function escape(html) {
|
|
||||||
return String(html)
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>');
|
|
||||||
}
|
|
44
test/context/error.js
Normal file
44
test/context/error.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
|
||||||
|
var context = require('../context');
|
||||||
|
var assert = require('assert');
|
||||||
|
|
||||||
|
describe('ctx.error(msg)', function(){
|
||||||
|
it('should set .status to 500', function(done){
|
||||||
|
var ctx = context();
|
||||||
|
|
||||||
|
try {
|
||||||
|
ctx.error('boom');
|
||||||
|
} catch (err) {
|
||||||
|
assert(500 == err.status);
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('ctx.error(msg, status)', function(){
|
||||||
|
it('should throw an error', function(done){
|
||||||
|
var ctx = context();
|
||||||
|
|
||||||
|
try {
|
||||||
|
ctx.error('name required', 400);
|
||||||
|
} catch (err) {
|
||||||
|
assert('name required' == err.message);
|
||||||
|
assert(400 == err.status);
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('ctx.error(status)', function(){
|
||||||
|
it('should throw an error', function(done){
|
||||||
|
var ctx = context();
|
||||||
|
|
||||||
|
try {
|
||||||
|
ctx.error(400);
|
||||||
|
} catch (err) {
|
||||||
|
assert('Bad Request' == err.message);
|
||||||
|
assert(400 == err.status);
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
18
test/request.js
Normal file
18
test/request.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
|
||||||
|
var Context = require('../lib/context');
|
||||||
|
var Request = require('../lib/request');
|
||||||
|
var koa = require('..');
|
||||||
|
|
||||||
|
function context(req, res) {
|
||||||
|
req = req || { headers: {} };
|
||||||
|
res = res || { _headers: {} };
|
||||||
|
res.setHeader = function(k, v){ res._headers[k.toLowerCase()] = v };
|
||||||
|
var ctx = new Context({}, req, res);
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
function request(req, res) {
|
||||||
|
return new Request(context(req, res));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = request;
|
78
test/request/accepts.js
Normal file
78
test/request/accepts.js
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
|
||||||
|
var context = require('../context');
|
||||||
|
|
||||||
|
describe('ctx.accepts(types)', function(){
|
||||||
|
describe('with no arguments', function(){
|
||||||
|
it('should return all accepted types', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.req.headers.accept = 'application/*;q=0.2, image/jpeg;q=0.8, text/html, text/plain';
|
||||||
|
ctx.accepts().should.eql(['text/html', 'text/plain', 'image/jpeg', 'application/*']);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with no valid types', function(){
|
||||||
|
it('should return false', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.accepts('', 'hey').should.be.false;
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when extensions are given', function(){
|
||||||
|
it('should convert to mime types', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.req.headers.accept = 'text/plain, text/html';
|
||||||
|
ctx.accepts('html').should.equal('html');
|
||||||
|
ctx.accepts('.html').should.equal('.html');
|
||||||
|
ctx.accepts('txt').should.equal('txt');
|
||||||
|
ctx.accepts('.txt').should.equal('.txt');
|
||||||
|
ctx.accepts('png').should.be.false;
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when an array is given', function(){
|
||||||
|
it('should return the first match', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.req.headers.accept = 'text/plain, text/html';
|
||||||
|
ctx.accepts(['png', 'text', 'html']).should.equal('text');
|
||||||
|
ctx.accepts(['png', 'html']).should.equal('html');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when multiple arguments are given', function(){
|
||||||
|
it('should return the first match', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.req.headers.accept = 'text/plain, text/html';
|
||||||
|
ctx.accepts('png', 'text', 'html').should.equal('text');
|
||||||
|
ctx.accepts('png', 'html').should.equal('html');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when present in Accept as an exact match', function(){
|
||||||
|
it('should return the type', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.req.headers.accept = 'text/plain, text/html';
|
||||||
|
ctx.accepts('text/html').should.equal('text/html');
|
||||||
|
ctx.accepts('text/plain').should.equal('text/plain');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when present in Accept as a type match', function(){
|
||||||
|
it('should return the type', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.req.headers.accept = 'application/json, */*';
|
||||||
|
ctx.accepts('text/html').should.equal('text/html');
|
||||||
|
ctx.accepts('text/plain').should.equal('text/plain');
|
||||||
|
ctx.accepts('image/png').should.equal('image/png');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when present in Accept as a subtype match', function(){
|
||||||
|
it('should return the type', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.req.headers.accept = 'application/json, text/*';
|
||||||
|
ctx.accepts('text/html').should.equal('text/html');
|
||||||
|
ctx.accepts('text/plain').should.equal('text/plain');
|
||||||
|
ctx.accepts('image/png').should.be.false;
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
37
test/request/acceptsCharsets.js
Normal file
37
test/request/acceptsCharsets.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
|
||||||
|
var context = require('../context');
|
||||||
|
|
||||||
|
describe('ctx.acceptsCharsets()', function(){
|
||||||
|
describe('with no arguments', function(){
|
||||||
|
describe('when Accept-Charset is populated', function(){
|
||||||
|
it('should return accepted types', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.req.headers['accept-charset'] = 'utf-8, iso-8859-1;q=0.2, utf-7;q=0.5';
|
||||||
|
ctx.acceptsCharsets().should.eql(['utf-8', 'utf-7', 'iso-8859-1']);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when Accept-Charset is not populated', function(){
|
||||||
|
it('should return an empty array', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.acceptsCharsets().should.eql([]);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with multiple arguments', function(){
|
||||||
|
it('should return the best fit', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.req.headers['accept-charset'] = 'utf-8, iso-8859-1;q=0.2, utf-7;q=0.5';
|
||||||
|
ctx.acceptsCharsets('utf-7', 'utf-8').should.equal('utf-8');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with an array', function(){
|
||||||
|
it('should return the best fit', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.req.headers['accept-charset'] = 'utf-8, iso-8859-1;q=0.2, utf-7;q=0.5';
|
||||||
|
ctx.acceptsCharsets(['utf-7', 'utf-8']).should.equal('utf-8');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
38
test/request/acceptsEncodings.js
Normal file
38
test/request/acceptsEncodings.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
|
||||||
|
var context = require('../context');
|
||||||
|
|
||||||
|
describe('ctx.acceptsEncodings()', function(){
|
||||||
|
describe('with no arguments', function(){
|
||||||
|
describe('when Accept-Encoding is populated', function(){
|
||||||
|
it('should return accepted types', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.req.headers['accept-encoding'] = 'gzip, compress;q=0.2';
|
||||||
|
ctx.acceptsEncodings().should.eql(['gzip', 'compress', 'identity']);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when Accept-Encoding is not populated', function(){
|
||||||
|
it('should return identity', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.acceptsEncodings().should.eql(['identity']);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with multiple arguments', function(){
|
||||||
|
it('should return the best fit', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.req.headers['accept-encoding'] = 'gzip, compress;q=0.2';
|
||||||
|
ctx.acceptsEncodings('compress', 'gzip').should.eql('gzip');
|
||||||
|
ctx.acceptsEncodings('gzip', 'compress').should.eql('gzip');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with an array', function(){
|
||||||
|
it('should return the best fit', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.req.headers['accept-encoding'] = 'gzip, compress;q=0.2';
|
||||||
|
ctx.acceptsEncodings(['compress', 'gzip']).should.eql('gzip');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
37
test/request/acceptsLanguages.js
Normal file
37
test/request/acceptsLanguages.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
|
||||||
|
var context = require('../context');
|
||||||
|
|
||||||
|
describe('ctx.acceptsLanguages(langs)', function(){
|
||||||
|
describe('with no arguments', function(){
|
||||||
|
describe('when Accept-Language is populated', function(){
|
||||||
|
it('should return accepted types', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.req.headers['accept-language'] = 'en;q=0.8, es, pt';
|
||||||
|
ctx.acceptsLanguages().should.eql(['es', 'pt', 'en']);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when Accept-Language is not populated', function(){
|
||||||
|
it('should return an empty array', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.acceptsLanguages().should.eql([]);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with multiple arguments', function(){
|
||||||
|
it('should return the best fit', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.req.headers['accept-language'] = 'en;q=0.8, es, pt';
|
||||||
|
ctx.acceptsLanguages('es', 'en').should.equal('es');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with an array', function(){
|
||||||
|
it('should return the best fit', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.req.headers['accept-language'] = 'en;q=0.8, es, pt';
|
||||||
|
ctx.acceptsLanguages(['es', 'en']).should.equal('es');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
39
test/request/fresh.js
Normal file
39
test/request/fresh.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
|
||||||
|
var context = require('../context');
|
||||||
|
|
||||||
|
describe('ctx.fresh', function(){
|
||||||
|
describe('the response is non-2xx', function(){
|
||||||
|
it('should return false', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.status = 404;
|
||||||
|
ctx.req.method = 'GET';
|
||||||
|
ctx.req.headers['if-none-match'] = '123';
|
||||||
|
ctx.set('ETag', '123');
|
||||||
|
ctx.fresh.should.be.false;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('the response is 2xx', function(){
|
||||||
|
describe('and etag matches', function(){
|
||||||
|
it('should return true', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.status = 200;
|
||||||
|
ctx.req.method = 'GET';
|
||||||
|
ctx.req.headers['if-none-match'] = '123';
|
||||||
|
ctx.set('ETag', '123');
|
||||||
|
ctx.fresh.should.be.true;
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('and etag do not match', function(){
|
||||||
|
it('should return false', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.status = 200;
|
||||||
|
ctx.req.method = 'GET';
|
||||||
|
ctx.req.headers['if-none-match'] = '123';
|
||||||
|
ctx.set('ETag', 'hey');
|
||||||
|
ctx.fresh.should.be.false;
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
12
test/request/get.js
Normal file
12
test/request/get.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
var context = require('../context');
|
||||||
|
|
||||||
|
describe('ctx.get(name)', function(){
|
||||||
|
it('should return the field value', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.req.headers.host = 'http://google.com';
|
||||||
|
ctx.get('HOST').should.equal('http://google.com');
|
||||||
|
ctx.get('Host').should.equal('http://google.com');
|
||||||
|
ctx.get('host').should.equal('http://google.com');
|
||||||
|
})
|
||||||
|
})
|
9
test/request/header.js
Normal file
9
test/request/header.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
|
||||||
|
var request = require('../request');
|
||||||
|
|
||||||
|
describe('req.header', function(){
|
||||||
|
it('should return the request header object', function(){
|
||||||
|
var req = request();
|
||||||
|
req.header.should.equal(req.req.headers);
|
||||||
|
})
|
||||||
|
})
|
31
test/request/host.js
Normal file
31
test/request/host.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
|
||||||
|
var request = require('../request');
|
||||||
|
|
||||||
|
describe('req.host', function(){
|
||||||
|
it('should return host void of port', function(){
|
||||||
|
var req = request();
|
||||||
|
req.header.host = 'foo.com:3000';
|
||||||
|
req.host.should.equal('foo.com');
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when X-Forwarded-Host is present', function(){
|
||||||
|
describe('and proxy is not trusted', function(){
|
||||||
|
it('should be ignored', function(){
|
||||||
|
var req = request();
|
||||||
|
req.header['x-forwarded-host'] = 'bar.com';
|
||||||
|
req.header['host'] = 'foo.com';
|
||||||
|
req.host.should.equal('foo.com')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('and proxy is trusted', function(){
|
||||||
|
it('should be used', function(){
|
||||||
|
var req = request();
|
||||||
|
req.app.proxy = true;
|
||||||
|
req.header['x-forwarded-host'] = 'bar.com, baz.com';
|
||||||
|
req.header['host'] = 'foo.com';
|
||||||
|
req.host.should.equal('bar.com')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
38
test/request/is.js
Normal file
38
test/request/is.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
|
||||||
|
var context = require('../context');
|
||||||
|
|
||||||
|
describe('ctx.is(type)', function(){
|
||||||
|
it('should ignore params', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.header['content-type'] = 'text/html; charset=utf-8';
|
||||||
|
ctx.is('text/*').should.be.true;
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('given a mime', function(){
|
||||||
|
it('should check the type', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.header['content-type'] = 'image/png';
|
||||||
|
|
||||||
|
ctx.is('image/png').should.be.true;
|
||||||
|
ctx.is('image/*').should.be.true;
|
||||||
|
ctx.is('*/png').should.be.true;
|
||||||
|
|
||||||
|
ctx.is('image/jpeg').should.be.false;
|
||||||
|
ctx.is('text/*').should.be.false;
|
||||||
|
ctx.is('*/jpeg').should.be.false;
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('given an extension', function(){
|
||||||
|
it('should check the type', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.header['content-type'] = 'image/png';
|
||||||
|
|
||||||
|
ctx.is('png').should.be.true;
|
||||||
|
ctx.is('.png').should.be.true;
|
||||||
|
|
||||||
|
ctx.is('jpeg').should.be.false;
|
||||||
|
ctx.is('.jpeg').should.be.false;
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
21
test/request/path.js
Normal file
21
test/request/path.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
|
||||||
|
var context = require('../context');
|
||||||
|
|
||||||
|
describe('ctx.path', function(){
|
||||||
|
it('should return the pathname', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.url = '/login?next=/dashboard';
|
||||||
|
ctx.path.should.equal('/login');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('ctx.path=', function(){
|
||||||
|
it('should set the pathname', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.url = '/login?next=/dashboard';
|
||||||
|
|
||||||
|
ctx.path = '/logout';
|
||||||
|
ctx.path.should.equal('/logout');
|
||||||
|
ctx.url.should.equal('/logout?next=/dashboard');
|
||||||
|
})
|
||||||
|
})
|
41
test/request/protocol.js
Normal file
41
test/request/protocol.js
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
|
||||||
|
var request = require('../request');
|
||||||
|
|
||||||
|
describe('req.protocol', function(){
|
||||||
|
describe('when encrypted', function(){
|
||||||
|
it('should return "https"', function(){
|
||||||
|
var req = request();
|
||||||
|
req.req.socket = { encrypted: true };
|
||||||
|
req.protocol.should.equal('https');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when unencrypted', function(){
|
||||||
|
it('should return "http"', function(){
|
||||||
|
var req = request();
|
||||||
|
req.req.socket = {};
|
||||||
|
req.protocol.should.equal('http');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when X-Forwarded-Proto is set', function(){
|
||||||
|
describe('and proxy is trusted', function(){
|
||||||
|
it('should be used', function(){
|
||||||
|
var req = request();
|
||||||
|
req.app.proxy = true;
|
||||||
|
req.req.socket = {};
|
||||||
|
req.header['x-forwarded-proto'] = 'https, http';
|
||||||
|
req.protocol.should.equal('https');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('and proxy is not trusted', function(){
|
||||||
|
it('should not be used', function(){
|
||||||
|
var req = request();
|
||||||
|
req.req.socket = {};
|
||||||
|
req.header['x-forwarded-proto'] = 'https, http';
|
||||||
|
req.protocol.should.equal('http');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
25
test/request/query.js
Normal file
25
test/request/query.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
|
||||||
|
var context = require('../context');
|
||||||
|
|
||||||
|
describe('ctx.query', function(){
|
||||||
|
describe('when missing', function(){
|
||||||
|
it('should return an empty object', function(){
|
||||||
|
var ctx = context({ url: '/' });
|
||||||
|
ctx.query.should.eql({});
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return a parsed query-string', function(){
|
||||||
|
var ctx = context({ url: '/?page=2' });
|
||||||
|
ctx.query.page.should.equal('2');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('ctx.query=', function(){
|
||||||
|
it('should stringify and replace the querystring', function(){
|
||||||
|
var ctx = context({ url: '/store/shoes' });
|
||||||
|
ctx.query = { page: 2, color: 'blue' };
|
||||||
|
ctx.url.should.equal('/store/shoes?page=2&color=blue');
|
||||||
|
ctx.querystring.should.equal('page=2&color=blue');
|
||||||
|
})
|
||||||
|
})
|
11
test/request/querystring.js
Normal file
11
test/request/querystring.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
|
||||||
|
var context = require('../context');
|
||||||
|
|
||||||
|
describe('ctx.querystring=', function(){
|
||||||
|
it('should replace the querystring', function(){
|
||||||
|
var ctx = context({ url: '/store/shoes' });
|
||||||
|
ctx.querystring = 'page=2&color=blue';
|
||||||
|
ctx.url.should.equal('/store/shoes?page=2&color=blue');
|
||||||
|
ctx.querystring.should.equal('page=2&color=blue');
|
||||||
|
})
|
||||||
|
})
|
10
test/request/secure.js
Normal file
10
test/request/secure.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
|
||||||
|
var request = require('../request');
|
||||||
|
|
||||||
|
describe('req.secure', function(){
|
||||||
|
it('should return true when encrypted', function(){
|
||||||
|
var req = request();
|
||||||
|
req.req.socket = { encrypted: true };
|
||||||
|
req.secure.should.be.true;
|
||||||
|
})
|
||||||
|
})
|
14
test/request/stale.js
Normal file
14
test/request/stale.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
|
||||||
|
var context = require('../context');
|
||||||
|
|
||||||
|
describe('req.stale', function(){
|
||||||
|
it('should be the inverse of req.fresh', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.status = 200;
|
||||||
|
ctx.method = 'GET';
|
||||||
|
ctx.req.headers['if-none-match'] = '"123"';
|
||||||
|
ctx.set('ETag', '"123"');
|
||||||
|
ctx.fresh.should.be.true;
|
||||||
|
ctx.stale.should.be.false;
|
||||||
|
})
|
||||||
|
})
|
18
test/response.js
Normal file
18
test/response.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
|
||||||
|
var Context = require('../lib/context');
|
||||||
|
var Response = require('../lib/response');
|
||||||
|
var koa = require('..');
|
||||||
|
|
||||||
|
function context(req, res) {
|
||||||
|
req = req || { headers: {} };
|
||||||
|
res = res || { _headers: {} };
|
||||||
|
res.setHeader = function(k, v){ res._headers[k.toLowerCase()] = v };
|
||||||
|
var ctx = new Context({}, req, res);
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
function response(req, res) {
|
||||||
|
return new Response(context(req, res));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = response;
|
21
test/response/attachment.js
Normal file
21
test/response/attachment.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
|
||||||
|
var context = require('../context');
|
||||||
|
|
||||||
|
describe('ctx.attachment([filename])', function(){
|
||||||
|
describe('when given a filename', function(){
|
||||||
|
it('should set the filename param', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.attachment('path/to/tobi.png');
|
||||||
|
var str = 'attachment; filename="tobi.png"';
|
||||||
|
ctx.response.header['content-disposition'].should.equal(str);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when omitting filename', function(){
|
||||||
|
it('should not set filename param', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.attachment();
|
||||||
|
ctx.response.header['content-disposition'].should.equal('attachment');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
101
test/response/body.js
Normal file
101
test/response/body.js
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
|
||||||
|
var response = require('../response');
|
||||||
|
var assert = require('assert');
|
||||||
|
var fs = require('fs');
|
||||||
|
|
||||||
|
describe('res.body=', function(){
|
||||||
|
describe('when Content-Type is set', function(){
|
||||||
|
it('should not override', function(){
|
||||||
|
var res = response();
|
||||||
|
res.type = 'png';
|
||||||
|
res.body = new Buffer('something');
|
||||||
|
assert('image/png' == res.header['content-type']);
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when body is an object', function(){
|
||||||
|
it('should override as json', function(){
|
||||||
|
var res = response();
|
||||||
|
|
||||||
|
res.body = '<em>hey</em>';
|
||||||
|
assert('text/html; charset=utf-8' == res.header['content-type']);
|
||||||
|
|
||||||
|
res.body = { foo: 'bar' };
|
||||||
|
assert('application/json' == res.header['content-type']);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should override length', function(){
|
||||||
|
var res = response();
|
||||||
|
res.type = 'html';
|
||||||
|
res.body = 'something';
|
||||||
|
res.length.should.equal(9);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when a string is given', function(){
|
||||||
|
it('should default to text', function(){
|
||||||
|
var res = response();
|
||||||
|
res.body = 'Tobi';
|
||||||
|
assert('text/plain; charset=utf-8' == res.header['content-type']);
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set length', function(){
|
||||||
|
var res = response();
|
||||||
|
res.body = 'Tobi';
|
||||||
|
assert('4' == res.header['content-length']);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when an html string is given', function(){
|
||||||
|
it('should default to html', function(){
|
||||||
|
var res = response();
|
||||||
|
res.body = '<h1>Tobi</h1>';
|
||||||
|
assert('text/html; charset=utf-8' == res.header['content-type']);
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set length', function(){
|
||||||
|
var string = '<h1>Tobi</h1>';
|
||||||
|
var res = response();
|
||||||
|
res.body = string;
|
||||||
|
assert.equal(res.length, Buffer.byteLength(string));
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set length when body is overriden', function(){
|
||||||
|
var string = '<h1>Tobi</h1>';
|
||||||
|
var res = response();
|
||||||
|
res.body = string;
|
||||||
|
res.body = string + string;
|
||||||
|
assert.equal(res.length, 2 * Buffer.byteLength(string));
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when a stream is given', function(){
|
||||||
|
it('should default to an octet stream', function(){
|
||||||
|
var res = response();
|
||||||
|
res.body = fs.createReadStream('LICENSE');
|
||||||
|
assert('application/octet-stream' == res.header['content-type']);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when a buffer is given', function(){
|
||||||
|
it('should default to an octet stream', function(){
|
||||||
|
var res = response();
|
||||||
|
res.body = new Buffer('hey');
|
||||||
|
assert('application/octet-stream' == res.header['content-type']);
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set length', function(){
|
||||||
|
var res = response();
|
||||||
|
res.body = new Buffer('Tobi');
|
||||||
|
assert('4' == res.header['content-length']);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when an object is given', function(){
|
||||||
|
it('should default to json', function(){
|
||||||
|
var res = response();
|
||||||
|
res.body = { foo: 'bar' };
|
||||||
|
assert('application/json' == res.header['content-type']);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
10
test/response/header.js
Normal file
10
test/response/header.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
|
||||||
|
var response = require('../response');
|
||||||
|
|
||||||
|
describe('res.header', function(){
|
||||||
|
it('should return the response header object', function(){
|
||||||
|
var res = response();
|
||||||
|
res.set('X-Foo', 'bar');
|
||||||
|
res.header.should.eql({ 'x-foo': 'bar' });
|
||||||
|
})
|
||||||
|
})
|
44
test/response/length.js
Normal file
44
test/response/length.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
|
||||||
|
var response = require('../response');
|
||||||
|
var assert = require('assert');
|
||||||
|
|
||||||
|
describe('res.length', function(){
|
||||||
|
describe('when Content-Length is defined', function(){
|
||||||
|
it('should return a number', function(){
|
||||||
|
var res = response();
|
||||||
|
res.header['content-length'] = '120';
|
||||||
|
res.length.should.equal(120);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('res.length', function(){
|
||||||
|
describe('when Content-Length is defined', function(){
|
||||||
|
it('should return a number', function(){
|
||||||
|
var res = response();
|
||||||
|
res.set('Content-Length', '1024');
|
||||||
|
res.length.should.equal(1024);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when Content-Length is not defined', function(){
|
||||||
|
describe('and a .body is set', function(){
|
||||||
|
it('should return a number', function(){
|
||||||
|
var res = response();
|
||||||
|
|
||||||
|
res.body = 'foo';
|
||||||
|
res.length.should.equal(3);
|
||||||
|
|
||||||
|
res.body = new Buffer('foo');
|
||||||
|
res.length.should.equal(3);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('and .body is not', function(){
|
||||||
|
it('should return undefined', function(){
|
||||||
|
var res = response();
|
||||||
|
assert(null == res.length);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
102
test/response/redirect.js
Normal file
102
test/response/redirect.js
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
|
||||||
|
var context = require('../context');
|
||||||
|
|
||||||
|
describe('ctx.redirect(url)', function(){
|
||||||
|
it('should redirect to the given url', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.redirect('http://google.com');
|
||||||
|
ctx.response.header.location.should.equal('http://google.com');
|
||||||
|
ctx.status.should.equal(302);
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with "back"', function(){
|
||||||
|
it('should redirect to Referrer', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.req.headers.referrer = '/login';
|
||||||
|
ctx.redirect('back');
|
||||||
|
ctx.response.header.location.should.equal('/login');
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should redirect to Referer', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.req.headers.referer = '/login';
|
||||||
|
ctx.redirect('back');
|
||||||
|
ctx.response.header.location.should.equal('/login');
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should default to alt', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.redirect('back', '/index.html');
|
||||||
|
ctx.response.header.location.should.equal('/index.html');
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should default redirect to /', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.redirect('back');
|
||||||
|
ctx.response.header.location.should.equal('/');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when html is accepted', function(){
|
||||||
|
it('should respond with html', function(){
|
||||||
|
var ctx = context();
|
||||||
|
var url = 'http://google.com';
|
||||||
|
ctx.header.accept = 'text/html';
|
||||||
|
ctx.redirect(url);
|
||||||
|
ctx.response.header['content-type'].should.equal('text/html; charset=utf-8');
|
||||||
|
ctx.body.should.equal('Redirecting to <a href="' + url + '">' + url + '</a>.');
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should escape the url', function(){
|
||||||
|
var ctx = context();
|
||||||
|
var url = '<script>';
|
||||||
|
ctx.header.accept = 'text/html';
|
||||||
|
ctx.redirect(url);
|
||||||
|
url = escape(url);
|
||||||
|
ctx.response.header['content-type'].should.equal('text/html; charset=utf-8');
|
||||||
|
ctx.body.should.equal('Redirecting to <a href="' + url + '">' + url + '</a>.');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when text is accepted', function(){
|
||||||
|
it('should respond with text', function(){
|
||||||
|
var ctx = context();
|
||||||
|
var url = 'http://google.com';
|
||||||
|
ctx.header.accept = 'text/plain';
|
||||||
|
ctx.redirect(url);
|
||||||
|
ctx.body.should.equal('Redirecting to ' + url + '.');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when status is 301', function(){
|
||||||
|
it('should not change the status code', function(){
|
||||||
|
var ctx = context();
|
||||||
|
var url = 'http://google.com';
|
||||||
|
ctx.status = 301;
|
||||||
|
ctx.header.accept = 'text/plain';
|
||||||
|
ctx.redirect('http://google.com');
|
||||||
|
ctx.status.should.equal(301);
|
||||||
|
ctx.body.should.equal('Redirecting to ' + url + '.');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when status is 304', function(){
|
||||||
|
it('should change the status code', function(){
|
||||||
|
var ctx = context();
|
||||||
|
var url = 'http://google.com';
|
||||||
|
ctx.status = 304;
|
||||||
|
ctx.header.accept = 'text/plain';
|
||||||
|
ctx.redirect('http://google.com');
|
||||||
|
ctx.status.should.equal(302);
|
||||||
|
ctx.body.should.equal('Redirecting to ' + url + '.');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function escape(html) {
|
||||||
|
return String(html)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>');
|
||||||
|
}
|
30
test/response/set.js
Normal file
30
test/response/set.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
|
||||||
|
var context = require('../context');
|
||||||
|
|
||||||
|
describe('ctx.set(name, val)', function(){
|
||||||
|
it('should set a field value', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.set('x-foo', 'bar');
|
||||||
|
ctx.response.header['x-foo'].should.equal('bar');
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should coerce to a string', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.set('x-foo', 5);
|
||||||
|
ctx.response.header['x-foo'].should.equal('5');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('ctx.set(object)', function(){
|
||||||
|
it('should set multiple fields', function(){
|
||||||
|
var ctx = context();
|
||||||
|
|
||||||
|
ctx.set({
|
||||||
|
foo: '1',
|
||||||
|
bar: '2'
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.response.header.foo.should.equal('1');
|
||||||
|
ctx.response.header.bar.should.equal('2');
|
||||||
|
})
|
||||||
|
})
|
70
test/response/status.js
Normal file
70
test/response/status.js
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
|
||||||
|
var response = require('../response');
|
||||||
|
var request = require('supertest');
|
||||||
|
var assert = require('assert');
|
||||||
|
var koa = require('../..');
|
||||||
|
|
||||||
|
describe('res.status=', function(){
|
||||||
|
describe('when a status string', function(){
|
||||||
|
describe('and valid', function(){
|
||||||
|
it('should set the status', function(){
|
||||||
|
var res = response();
|
||||||
|
res.status = 'forbidden';
|
||||||
|
res.status.should.equal(403);
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be case-insensitive', function(){
|
||||||
|
var res = response();
|
||||||
|
res.status = 'ForBidden';
|
||||||
|
res.status.should.equal(403);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('and invalid', function(){
|
||||||
|
it('should throw', function(){
|
||||||
|
var res = response();
|
||||||
|
var err;
|
||||||
|
|
||||||
|
try {
|
||||||
|
res.status = 'maru';
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(err);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function strip(status) {
|
||||||
|
it('should strip content related header fields', function(done){
|
||||||
|
var app = koa();
|
||||||
|
|
||||||
|
app.use(function(next){
|
||||||
|
return function *(){
|
||||||
|
this.body = { foo: 'bar' };
|
||||||
|
this.set('Content-Type', 'application/json');
|
||||||
|
this.set('Content-Length', '15');
|
||||||
|
this.set('Transfer-Encoding', 'chunked');
|
||||||
|
this.status = status;
|
||||||
|
assert(null == this.response.header['content-type']);
|
||||||
|
assert(null == this.response.header['content-length']);
|
||||||
|
assert(null == this.response.header['transfer-encoding']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
request(app.listen())
|
||||||
|
.get('/')
|
||||||
|
.expect(status)
|
||||||
|
.end(done);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('when 204', function(){
|
||||||
|
strip(204);
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when 304', function(){
|
||||||
|
strip(304);
|
||||||
|
})
|
||||||
|
})
|
38
test/response/type.js
Normal file
38
test/response/type.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
|
||||||
|
var context = require('../context');
|
||||||
|
var assert = require('assert');
|
||||||
|
|
||||||
|
describe('ctx.type=', function(){
|
||||||
|
describe('with a mime', function(){
|
||||||
|
it('should set the Content-Type', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.type = 'text/plain';
|
||||||
|
ctx.response.header['content-type'].should.equal('text/plain');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with an extension', function(){
|
||||||
|
it('should lookup the mime', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.type = 'json';
|
||||||
|
ctx.response.header['content-type'].should.equal('application/json');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('ctx.type', function(){
|
||||||
|
describe('with no Content-Type', function(){
|
||||||
|
it('should return null', function(){
|
||||||
|
var ctx = context();
|
||||||
|
assert(null == ctx.type);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with a Content-Type', function(){
|
||||||
|
it('should return the mime', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.req.headers['content-type'] = 'text/html; charset=utf8';
|
||||||
|
ctx.type.should.equal('text/html');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
32
test/response/vary.js
Normal file
32
test/response/vary.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
|
||||||
|
var context = require('../context');
|
||||||
|
|
||||||
|
describe('ctx.vary(field)', function(){
|
||||||
|
describe('when Vary is not set', function(){
|
||||||
|
it('should set it', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.vary('Accept');
|
||||||
|
ctx.response.header.vary.should.equal('Accept');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when Vary is set', function(){
|
||||||
|
it('should append', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.vary('Accept');
|
||||||
|
ctx.vary('Accept-Encoding');
|
||||||
|
ctx.response.header.vary.should.equal('Accept, Accept-Encoding');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when Vary already contains the value', function(){
|
||||||
|
it('should not append', function(){
|
||||||
|
var ctx = context();
|
||||||
|
ctx.vary('Accept');
|
||||||
|
ctx.vary('Accept-Encoding');
|
||||||
|
ctx.vary('Accept');
|
||||||
|
ctx.vary('Accept-Encoding');
|
||||||
|
ctx.response.header.vary.should.equal('Accept, Accept-Encoding');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in a new issue