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 \
|
||||
--require should \
|
||||
--harmony-generators \
|
||||
test/context/* \
|
||||
test/request/* \
|
||||
test/response/* \
|
||||
--bail
|
||||
|
||||
bench:
|
||||
|
|
|
@ -26,7 +26,7 @@ alias node='node --harmony-generators'
|
|||
|
||||
## Community
|
||||
|
||||
- [API](docs/api.md) documentation
|
||||
- [API](docs/api/index.md) documentation
|
||||
- [Middleware](https://github.com/koajs/koa/wiki) list
|
||||
- [Wiki](https://github.com/koajs/koa/wiki)
|
||||
- [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;
|
||||
}
|
||||
|
||||
describe('ctx.body=', function(){
|
||||
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, '>');
|
||||
}
|
||||
module.exports = context;
|
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