Skip to content

Commit cc8b8fe

Browse files
committed
Early 304. Closes #3286
1 parent 0272560 commit cc8b8fe

10 files changed

+301
-67
lines changed

API.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# 14.1.x API Reference
1+
# 14.2.x API Reference
22

33
- [Server](#server)
44
- [`new Server([options])`](#new-serveroptions)
@@ -3436,6 +3436,45 @@ const onRequest = function (request, reply) {
34363436
server.ext('onRequest', onRequest);
34373437
```
34383438

3439+
### `reply.entity(options)`
3440+
3441+
Sets the response 'ETag' and 'Last-Modified' headers and checks for any conditional request headers to
3442+
decide if the response is going to qualify for an HTTP 304 (Not Modified). If the entity values match
3443+
the request conditions, `reply.entity()` returns control back to the framework with a 304 response.
3444+
Otherwise, it sets the provided entity headers and returns `null`, where:
3445+
- `options` - a required configuration object with:
3446+
- `etag` - the ETag string. Required if `modified` is not present. Defaults to no header.
3447+
- `modified` - the Last-Modified header value. Required if `etag` is not present. Defaults to no header.
3448+
- `vary` - same as the `response.etag()` option. Defaults to `true`.
3449+
3450+
Returns a response object if the reply is unmodified or `null` if the response has changed. If `null` is
3451+
returned, the developer must call `reply()` to continue execution. If the response is not `null`, the developer
3452+
must not call `reply()`.
3453+
3454+
```js
3455+
const Hapi = require('hapi');
3456+
const server = new Hapi.Server();
3457+
server.connection({ port: 80 });
3458+
3459+
server.route({
3460+
method: 'GET',
3461+
path: '/',
3462+
config: {
3463+
cache: { expiresIn: 5000 },
3464+
handler: function (request, reply) {
3465+
3466+
const response = reply.entity({ etag: 'abc' });
3467+
if (response) {
3468+
response.header('X', 'y');
3469+
return;
3470+
}
3471+
3472+
return reply('ok');
3473+
}
3474+
}
3475+
});
3476+
```
3477+
34393478
### `reply.close([options])`
34403479

34413480
Concludes the handler activity by returning control over to the router and informing the router

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Lead Maintainer: [Eran Hammer](https://github.com/hueniverse)
1212
authentication, and other essential facilities for building web and services applications. **hapi** enables
1313
developers to focus on writing reusable application logic in a highly modular and prescriptive approach.
1414

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

1818
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

lib/handler.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,11 @@ internals.prerequisites = function (request, callback) {
4545
return next(err);
4646
}
4747

48-
if (!result._takeover) {
49-
return next();
48+
if (result._takeover) {
49+
return callback(null, result);
5050
}
5151

52-
return callback(null, result);
52+
return next();
5353
});
5454
}, nextSet);
5555
};

lib/reply.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,13 @@ internals.Reply.prototype.interface = function (request, realm, next) { //
7676
reply.realm = realm;
7777
reply.request = request;
7878

79-
reply.response = internals.response;
8079
reply.close = internals.close;
80+
reply.continue = internals.continue;
8181
reply.state = internals.state;
8282
reply.unstate = internals.unstate;
8383
reply.redirect = internals.redirect;
84-
reply.continue = internals.continue;
84+
reply.response = internals.response;
85+
reply.entity = internals.entity;
8586

8687
if (this._decorations) {
8788
const methods = Object.keys(this._decorations);
@@ -172,3 +173,18 @@ internals.hold = function (reply) {
172173
return this;
173174
};
174175
};
176+
177+
178+
internals.entity = function (options) {
179+
180+
Hoek.assert(options, 'Entity method missing required options');
181+
Hoek.assert(options.etag || options.modified, 'Entity methods missing require options key');
182+
183+
this.request._entity = options;
184+
185+
if (Response.unmodified(this.request, options)) {
186+
return this.response().code(304).takeover();
187+
}
188+
189+
return null;
190+
};

lib/request.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ internals.Request = function (connection, req, res, options) {
143143
// Private members
144144

145145
this._states = {};
146+
this._entity = {}; // Entity information set via reply.entity()
146147
this._logger = [];
147148
this._allowInternals = !!options.allowInternals;
148149
this._isPayloadPending = true; // false when incoming payload fully processed

lib/response.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,70 @@ internals.Response.prototype.etag = function (tag, options) {
198198
};
199199

200200

201+
internals.Response.unmodified = function (request, options) {
202+
203+
if (request.method !== 'get' &&
204+
request.method !== 'head') {
205+
206+
return false;
207+
}
208+
209+
// Strong verifier
210+
211+
if (options.etag &&
212+
request.headers['if-none-match']) {
213+
214+
const ifNoneMatch = request.headers['if-none-match'].split(/\s*,\s*/);
215+
for (let i = 0; i < ifNoneMatch.length; ++i) {
216+
const etag = ifNoneMatch[i];
217+
if (etag === options.etag) {
218+
return true;
219+
}
220+
221+
if (options.vary) {
222+
const etagBase = options.etag.slice(0, -1);
223+
if (etag === etagBase + '-gzip"' ||
224+
etag === etagBase + '-deflate"') {
225+
226+
return true;
227+
}
228+
}
229+
}
230+
231+
return false;
232+
}
233+
234+
// Weak verifier
235+
236+
const ifModifiedSinceHeader = request.headers['if-modified-since'];
237+
238+
if (ifModifiedSinceHeader &&
239+
options.modified) {
240+
241+
const ifModifiedSince = internals.parseDate(ifModifiedSinceHeader);
242+
const lastModified = internals.parseDate(options.modified);
243+
244+
if (ifModifiedSince &&
245+
lastModified &&
246+
ifModifiedSince >= lastModified) {
247+
248+
return true;
249+
}
250+
}
251+
252+
return false;
253+
};
254+
255+
256+
internals.parseDate = function (string) {
257+
258+
try {
259+
return Date.parse(string);
260+
}
261+
catch (errIgnore) { }
262+
};
263+
264+
201265
internals.Response.prototype.type = function (type) {
202266

203267
this._header('content-type', type);

lib/transmit.js

Lines changed: 35 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -54,54 +54,7 @@ internals.marshal = function (request, next) {
5454
Cors.headers(response);
5555
internals.content(response, false);
5656
internals.security(response);
57-
58-
if (response.statusCode !== 304 &&
59-
(request.method === 'get' || request.method === 'head')) {
60-
61-
if (response.headers.etag &&
62-
request.headers['if-none-match']) {
63-
64-
// Strong verifier
65-
66-
const ifNoneMatch = request.headers['if-none-match'].split(/\s*,\s*/);
67-
for (let i = 0; i < ifNoneMatch.length; ++i) {
68-
const etag = ifNoneMatch[i];
69-
if (etag === response.headers.etag) {
70-
response.code(304);
71-
break;
72-
}
73-
else if (response.settings.varyEtag) {
74-
const etagBase = response.headers.etag.slice(0, -1);
75-
if (etag === etagBase + '-gzip"' ||
76-
etag === etagBase + '-deflate"') {
77-
78-
response.code(304);
79-
break;
80-
}
81-
}
82-
}
83-
}
84-
else {
85-
const ifModifiedSinceHeader = request.headers['if-modified-since'];
86-
const lastModifiedHeader = response.headers['last-modified'];
87-
88-
if (ifModifiedSinceHeader &&
89-
lastModifiedHeader) {
90-
91-
// Weak verifier
92-
93-
const ifModifiedSince = internals.parseDate(ifModifiedSinceHeader);
94-
const lastModified = internals.parseDate(lastModifiedHeader);
95-
96-
if (ifModifiedSince &&
97-
lastModified &&
98-
ifModifiedSince >= lastModified) {
99-
100-
response.code(304);
101-
}
102-
}
103-
}
104-
}
57+
internals.unmodified(response);
10558

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

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

158111

159-
internals.parseDate = function (string) {
160-
161-
try {
162-
return Date.parse(string);
163-
}
164-
catch (errIgnore) { }
165-
};
166-
167-
168112
internals.fail = function (request, boom, callback) {
169113

170114
const error = boom.output;
@@ -565,3 +509,37 @@ internals.state = function (response, next) {
565509
});
566510
});
567511
};
512+
513+
514+
internals.unmodified = function (response) {
515+
516+
const request = response.request;
517+
518+
// Set headers from reply.entity()
519+
520+
if (request._entity.etag &&
521+
!response.headers.etag) {
522+
523+
response.etag(request._entity.etag, { vary: request._entity.vary });
524+
}
525+
526+
if (request._entity.modified &&
527+
!response.headers['last-modified']) {
528+
529+
response.header('last-modified', request._entity.modified);
530+
}
531+
532+
if (response.statusCode === 304) {
533+
return;
534+
}
535+
536+
const entity = {
537+
etag: response.headers.etag,
538+
vary: response.settings.varyEtag,
539+
modified: response.headers['last-modified']
540+
};
541+
542+
if (Response.unmodified(request, entity)) {
543+
response.code(304);
544+
}
545+
};

npm-shrinkwrap.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "hapi",
33
"description": "HTTP Server framework",
44
"homepage": "http://hapijs.com",
5-
"version": "14.1.0",
5+
"version": "14.2.0",
66
"repository": {
77
"type": "git",
88
"url": "git://github.com/hapijs/hapi"

0 commit comments

Comments
 (0)