Skip to content

Commit

Permalink
Early 304. Closes #3286
Browse files Browse the repository at this point in the history
  • Loading branch information
hueniverse committed Aug 13, 2016
1 parent 0272560 commit cc8b8fe
Show file tree
Hide file tree
Showing 10 changed files with 301 additions and 67 deletions.
41 changes: 40 additions & 1 deletion API.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 14.1.x API Reference
# 14.2.x API Reference

- [Server](#server)
- [`new Server([options])`](#new-serveroptions)
Expand Down Expand Up @@ -3436,6 +3436,45 @@ const onRequest = function (request, reply) {
server.ext('onRequest', onRequest);
```

### `reply.entity(options)`

Sets the response 'ETag' and 'Last-Modified' headers and checks for any conditional request headers to
decide if the response is going to qualify for an HTTP 304 (Not Modified). If the entity values match
the request conditions, `reply.entity()` returns control back to the framework with a 304 response.
Otherwise, it sets the provided entity headers and returns `null`, where:
- `options` - a required configuration object with:
- `etag` - the ETag string. Required if `modified` is not present. Defaults to no header.
- `modified` - the Last-Modified header value. Required if `etag` is not present. Defaults to no header.
- `vary` - same as the `response.etag()` option. Defaults to `true`.

Returns a response object if the reply is unmodified or `null` if the response has changed. If `null` is
returned, the developer must call `reply()` to continue execution. If the response is not `null`, the developer
must not call `reply()`.

```js
const Hapi = require('hapi');
const server = new Hapi.Server();
server.connection({ port: 80 });

server.route({
method: 'GET',
path: '/',
config: {
cache: { expiresIn: 5000 },
handler: function (request, reply) {

const response = reply.entity({ etag: 'abc' });
if (response) {
response.header('X', 'y');
return;
}

return reply('ok');
}
}
});
```

### `reply.close([options])`

Concludes the handler activity by returning control over to the router and informing the router
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Lead Maintainer: [Eran Hammer](https://github.com/hueniverse)
authentication, and other essential facilities for building web and services applications. **hapi** enables
developers to focus on writing reusable application logic in a highly modular and prescriptive approach.

Development version: **14.0.x** ([release notes](https://github.com/hapijs/hapi/issues?labels=release+notes&page=1&state=closed))
Development version: **14.2.x** ([release notes](https://github.com/hapijs/hapi/issues?labels=release+notes&page=1&state=closed))
[![Build Status](https://secure.travis-ci.org/hapijs/hapi.svg?branch=master)](http://travis-ci.org/hapijs/hapi)

For the latest updates, [change log](http://hapijs.com/updates), and release information visit [hapijs.com](http://hapijs.com) and follow [@hapijs](https://twitter.com/hapijs) on twitter. If you have questions, please open an issue in the
Expand Down
6 changes: 3 additions & 3 deletions lib/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ internals.prerequisites = function (request, callback) {
return next(err);
}

if (!result._takeover) {
return next();
if (result._takeover) {
return callback(null, result);
}

return callback(null, result);
return next();
});
}, nextSet);
};
Expand Down
20 changes: 18 additions & 2 deletions lib/reply.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,13 @@ internals.Reply.prototype.interface = function (request, realm, next) { //
reply.realm = realm;
reply.request = request;

reply.response = internals.response;
reply.close = internals.close;
reply.continue = internals.continue;
reply.state = internals.state;
reply.unstate = internals.unstate;
reply.redirect = internals.redirect;
reply.continue = internals.continue;
reply.response = internals.response;
reply.entity = internals.entity;

if (this._decorations) {
const methods = Object.keys(this._decorations);
Expand Down Expand Up @@ -172,3 +173,18 @@ internals.hold = function (reply) {
return this;
};
};


internals.entity = function (options) {

Hoek.assert(options, 'Entity method missing required options');
Hoek.assert(options.etag || options.modified, 'Entity methods missing require options key');

this.request._entity = options;

if (Response.unmodified(this.request, options)) {
return this.response().code(304).takeover();
}

return null;
};
1 change: 1 addition & 0 deletions lib/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ internals.Request = function (connection, req, res, options) {
// Private members

this._states = {};
this._entity = {}; // Entity information set via reply.entity()
this._logger = [];
this._allowInternals = !!options.allowInternals;
this._isPayloadPending = true; // false when incoming payload fully processed
Expand Down
64 changes: 64 additions & 0 deletions lib/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,70 @@ internals.Response.prototype.etag = function (tag, options) {
};


internals.Response.unmodified = function (request, options) {

if (request.method !== 'get' &&
request.method !== 'head') {

return false;
}

// Strong verifier

if (options.etag &&
request.headers['if-none-match']) {

const ifNoneMatch = request.headers['if-none-match'].split(/\s*,\s*/);
for (let i = 0; i < ifNoneMatch.length; ++i) {
const etag = ifNoneMatch[i];
if (etag === options.etag) {
return true;
}

if (options.vary) {
const etagBase = options.etag.slice(0, -1);
if (etag === etagBase + '-gzip"' ||
etag === etagBase + '-deflate"') {

return true;
}
}
}

return false;
}

// Weak verifier

const ifModifiedSinceHeader = request.headers['if-modified-since'];

if (ifModifiedSinceHeader &&
options.modified) {

const ifModifiedSince = internals.parseDate(ifModifiedSinceHeader);
const lastModified = internals.parseDate(options.modified);

if (ifModifiedSince &&
lastModified &&
ifModifiedSince >= lastModified) {

return true;
}
}

return false;
};


internals.parseDate = function (string) {

try {
return Date.parse(string);
}
catch (errIgnore) { }
};


internals.Response.prototype.type = function (type) {

this._header('content-type', type);
Expand Down
92 changes: 35 additions & 57 deletions lib/transmit.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,54 +54,7 @@ internals.marshal = function (request, next) {
Cors.headers(response);
internals.content(response, false);
internals.security(response);

if (response.statusCode !== 304 &&
(request.method === 'get' || request.method === 'head')) {

if (response.headers.etag &&
request.headers['if-none-match']) {

// Strong verifier

const ifNoneMatch = request.headers['if-none-match'].split(/\s*,\s*/);
for (let i = 0; i < ifNoneMatch.length; ++i) {
const etag = ifNoneMatch[i];
if (etag === response.headers.etag) {
response.code(304);
break;
}
else if (response.settings.varyEtag) {
const etagBase = response.headers.etag.slice(0, -1);
if (etag === etagBase + '-gzip"' ||
etag === etagBase + '-deflate"') {

response.code(304);
break;
}
}
}
}
else {
const ifModifiedSinceHeader = request.headers['if-modified-since'];
const lastModifiedHeader = response.headers['last-modified'];

if (ifModifiedSinceHeader &&
lastModifiedHeader) {

// Weak verifier

const ifModifiedSince = internals.parseDate(ifModifiedSinceHeader);
const lastModified = internals.parseDate(lastModifiedHeader);

if (ifModifiedSince &&
lastModified &&
ifModifiedSince >= lastModified) {

response.code(304);
}
}
}
}
internals.unmodified(response);

internals.state(response, (err) => {

Expand Down Expand Up @@ -156,15 +109,6 @@ internals.marshal = function (request, next) {
};


internals.parseDate = function (string) {

try {
return Date.parse(string);
}
catch (errIgnore) { }
};


internals.fail = function (request, boom, callback) {

const error = boom.output;
Expand Down Expand Up @@ -565,3 +509,37 @@ internals.state = function (response, next) {
});
});
};


internals.unmodified = function (response) {

const request = response.request;

// Set headers from reply.entity()

if (request._entity.etag &&
!response.headers.etag) {

response.etag(request._entity.etag, { vary: request._entity.vary });
}

if (request._entity.modified &&
!response.headers['last-modified']) {

response.header('last-modified', request._entity.modified);
}

if (response.statusCode === 304) {
return;
}

const entity = {
etag: response.headers.etag,
vary: response.settings.varyEtag,
modified: response.headers['last-modified']
};

if (Response.unmodified(request, entity)) {
response.code(304);
}
};
2 changes: 1 addition & 1 deletion npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "hapi",
"description": "HTTP Server framework",
"homepage": "http://hapijs.com",
"version": "14.1.0",
"version": "14.2.0",
"repository": {
"type": "git",
"url": "git://github.com/hapijs/hapi"
Expand Down
Loading

0 comments on commit cc8b8fe

Please sign in to comment.