From d498c5a2206c5f3b2ae530c71552c1408079e880 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Fri, 16 Nov 2012 10:32:27 -0800 Subject: [PATCH 1/8] Add Direct response outline, fix request execute order --- lib/request.js | 38 +++++++++++------------ lib/response/direct.js | 69 ++++++++++++++++++++++++++++++++++++++++++ lib/response/index.js | 1 + 3 files changed, 89 insertions(+), 19 deletions(-) create mode 100755 lib/response/direct.js diff --git a/lib/request.js b/lib/request.js index 367301531..0ce89f48b 100755 --- a/lib/request.js +++ b/lib/request.js @@ -218,8 +218,8 @@ internals.Request.prototype._execute = function (route) { var funcs = [ // ext.onRequest() in Server - internals.processDebug, internals.authenticate, + internals.processDebug, Validation.query, Payload.read, Validation.payload, @@ -274,6 +274,24 @@ internals.authenticate = function (request, next) { }; +internals.processDebug = function (request, next) { + + // Extract session debugging information + + if (request.server.settings.debug) { + if (request.query[request.server.settings.debug.queryKey]) { + delete request.url.search; + delete request.query[request.server.settings.debug.queryKey]; + + request.raw.req.url = Url.format(request.url); + request._setUrl(request.raw.req.url); + } + } + + return next(); +}; + + internals.Request.prototype._decorate = function (callback) { var self = this; @@ -565,21 +583,3 @@ internals.Request.prototype._wagTail = function () { this.server.emit('tail', this); } }; - - -internals.processDebug = function (request, next) { - - // Extract session debugging information - - if (request.server.settings.debug) { - if (request.query[request.server.settings.debug.queryKey]) { - delete request.url.search; - delete request.query[request.server.settings.debug.queryKey]; - - request.raw.req.url = Url.format(request.url); - request._setUrl(request.raw.req.url); - } - } - - return next(); -}; diff --git a/lib/response/direct.js b/lib/response/direct.js new file mode 100755 index 000000000..613a58ec9 --- /dev/null +++ b/lib/response/direct.js @@ -0,0 +1,69 @@ +// Load modules + +var NodeUtil = require('util'); +var Base = require('./base'); + + +// Declare internals + +var internals = {}; + + +// Direct response + +exports = module.exports = internals.Direct = function (request) { + + Base.call(this); + this._tag = 'chunked'; + this._request = request; + + return this; +}; + +NodeUtil.inherits(internals.Direct, Base); + + +internals.Direct.prototype._transmit = function (request, callback) { + + request.raw.res.writeHead(this.code, this.headers); + request.raw.res.end(request.method !== 'head' ? this.payload : ''); + + return callback(); +}; + + +internals.Direct.prototype.header = function (key, value) { + + this.headers[key] = value; + return this; +}; + + +internals.Direct.prototype.type = function (type) { + + this.headers['Content-Type'] = type; + return this; +}; + + +internals.Direct.prototype.bytes = function (bytes) { + + this.headers['Content-Length'] = bytes; + return this; +}; + + +internals.Direct.prototype.created = function (uri) { + + this.code = 201; + this.headers['Location'] = uri; + return this; +}; + + +internals.Direct.prototype.ttl = function (ttlMsec, isOverride) { // isOverride defaults to true + + this.ttlMsec = (isOverride === false ? (this.ttlMsec ? this.ttlMsec : ttlMsec) : ttlMsec); + return this; +}; + diff --git a/lib/response/index.js b/lib/response/index.js index 18b15e38a..a1a8a526f 100755 --- a/lib/response/index.js +++ b/lib/response/index.js @@ -20,6 +20,7 @@ exports.Obj = internals.Obj = require('./obj'); exports.Text = internals.Text = require('./text'); exports.Stream = internals.Stream = require('./stream'); exports.File = internals.File = require('./file'); +exports.Direct = internals.Direct = require('./direct'); internals.Error = require('./error'); From 5c8bd3b3652051f0e74369a8d7aebffd6bef3491 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Fri, 16 Nov 2012 22:51:11 -0800 Subject: [PATCH 2/8] Direct response --- lib/cache/stale.js | 9 ++-- lib/request.js | 61 ++++++++++++-------------- lib/response/base.js | 2 + lib/response/direct.js | 85 ++++++++++++++++++++++++++++++------ lib/response/empty.js | 2 +- lib/response/error.js | 2 +- lib/response/generic.js | 18 ++++---- lib/response/headers.js | 46 +++++++++++++++++++ lib/response/index.js | 80 +++++++++++---------------------- lib/response/obj.js | 4 +- lib/response/stream.js | 6 +-- lib/response/text.js | 4 +- test/integration/cache.js | 6 +-- test/integration/response.js | 20 ++++++--- 14 files changed, 211 insertions(+), 134 deletions(-) create mode 100755 lib/response/headers.js diff --git a/lib/cache/stale.js b/lib/cache/stale.js index f851404a9..38f92fd6a 100755 --- a/lib/cache/stale.js +++ b/lib/cache/stale.js @@ -98,8 +98,7 @@ exports.process = function (cache, key, logFunc, baseTags, generateFunc, callbac // Set stale timeout - cached.ttl -= cache.rule.staleTimeout; // Adjust TTL for when the timeout is invoked - setTimeout(function () { + function timerFunc() { if (wasCallbackCalled) { return; @@ -107,8 +106,10 @@ exports.process = function (cache, key, logFunc, baseTags, generateFunc, callbac wasCallbackCalled = true; return callback(cached.item, cached); - }, - cache.rule.staleTimeout); + }; + + cached.ttl -= cache.rule.staleTimeout; // Adjust TTL for when the timeout is invoked + setTimeout(timerFunc, cache.rule.staleTimeout); } // Generate new value diff --git a/lib/request.js b/lib/request.js index 0ce89f48b..d6194e54c 100755 --- a/lib/request.js +++ b/lib/request.js @@ -384,49 +384,40 @@ internals.Request.prototype._unknown = function () { }; -// Request handler - internals.handler = function (request, next) { - // Log interface helper - - var logFunc = function () { + var lookup = function () { - return request.log.apply(request, arguments); - }; + var logFunc = function () { - // Get from cache + return request.log.apply(request, arguments); + }; - request._route.cache.getOrGenerate(request.url.path, logFunc, request._generateResponse(), function (response, cached) { // request.url.path contains query + request._route.cache.getOrGenerate(request.url.path, logFunc, generate, function (response, cached) { // request.url.path contains query - // Set TTL + // Set TTL - if (!(response instanceof Error)) { - if (cached) { - response.ttl(cached.ttl); - } - else if (request._route.cache.isMode('client')) { - response.ttl(request._route.cache.ttl(), false); // Does not override ttl is already set + if (!(response instanceof Error)) { + if (cached) { + response.ttl(cached.ttl); + } + else if (request._route.cache.isMode('client')) { + response.ttl(request._route.cache.ttl(), false); // Does not override ttl is already set + } } - } - - // Store response - request.response = response; - return next(); - }); -}; - - -internals.Request.prototype._generateResponse = function () { + // Store response - var self = this; + request.response = response; + return next(); + }); + }; - return function (callback) { + var generate = function (callback) { // Execute prerequisites - self._prerequisites(function (err) { + request._prerequisites(function (err) { if (err) { return callback(err); @@ -435,24 +426,26 @@ internals.Request.prototype._generateResponse = function () { // Decorate request with helper functions var timer = new Utils.Timer(); - self._decorate(function (response) { + request._decorate(function (response) { // Check for Error result if (response instanceof Error) { - self.log(['handler', 'result', 'error'], { msec: timer.elapsed() }); + request.log(['handler', 'result', 'error'], { msec: timer.elapsed() }); return callback(response); } - self.log(['handler', 'result'], { msec: timer.elapsed() }); - return callback(null, response, response.ttlMsec); + request.log(['handler', 'result'], { msec: timer.elapsed() }); + return callback(null, response, response._ttl); // Not all response types include _ttl }); // Execute handler - self._route.config.handler(self); + request._route.config.handler(request); }); }; + + lookup(); }; diff --git a/lib/response/base.js b/lib/response/base.js index 7e0e7660c..01ae09e88 100755 --- a/lib/response/base.js +++ b/lib/response/base.js @@ -15,6 +15,8 @@ exports = module.exports = internals.Base = function () { Utils.assert(this.constructor !== internals.Base, 'Base must not be instantiated directly'); this._tag = 'base'; + this.isCacheable = false; + return this; }; diff --git a/lib/response/direct.js b/lib/response/direct.js index 613a58ec9..f022df8e9 100755 --- a/lib/response/direct.js +++ b/lib/response/direct.js @@ -2,11 +2,14 @@ var NodeUtil = require('util'); var Base = require('./base'); +var Headers = require('./headers'); // Declare internals -var internals = {}; +var internals = { + decorators: ['code', 'header', 'type', 'bytes', 'created', 'ttl'] +}; // Direct response @@ -15,7 +18,18 @@ exports = module.exports = internals.Direct = function (request) { Base.call(this); this._tag = 'chunked'; + this._request = request; + this._request.raw.res.statusCode = 200; + this._isDecorated = true; + + // Decorate + + this.write = internals.write; + for (var i = 0, il = internals.decorators.length; i < il; ++i) { + var name = internals.decorators[i]; + this[name] = internals[name]; + } return this; }; @@ -23,47 +37,90 @@ exports = module.exports = internals.Direct = function (request) { NodeUtil.inherits(internals.Direct, Base); +internals.Direct.prototype._undecorate = function () { + + if (!this._isDecorated) { + return; + } + + this._isDecorated = false; + + for (var i = 0, il = internals.decorators.length; i < il; ++i) { + var name = internals.decorators[i]; + delete this[name]; + } +}; + + internals.Direct.prototype._transmit = function (request, callback) { - request.raw.res.writeHead(this.code, this.headers); - request.raw.res.end(request.method !== 'head' ? this.payload : ''); + this._undecorate(); + delete this.write; + + this._request.raw.res.end(); return callback(); }; -internals.Direct.prototype.header = function (key, value) { +// Decorators + +internals.write = function (chunk, encoding) { + + if (this._isDecorated) { + Headers.set(this, this._request); // First write - set Cache, CORS headers + } + + this._undecorate(); + + if (this._request.method !== 'head') { + + this._request.raw.res.write(chunk, encoding); + } + + return this; +}; + + +internals.code = function (code) { + + this._request.raw.res.statusCode = code; + return this; +}; + + +internals.header = function (key, value) { - this.headers[key] = value; + this._request.raw.res.setHeader(key, value); return this; }; -internals.Direct.prototype.type = function (type) { +internals.type = function (type) { - this.headers['Content-Type'] = type; + this._request.raw.res.setHeader('Content-Type', type); return this; }; -internals.Direct.prototype.bytes = function (bytes) { +internals.bytes = function (bytes) { - this.headers['Content-Length'] = bytes; + this._request.raw.res.setHeader('Content-Length', bytes); return this; }; -internals.Direct.prototype.created = function (uri) { +internals.created = function (uri) { - this.code = 201; - this.headers['Location'] = uri; + this._code = 201; + this._request.raw.res.setHeader('Location', Headers.location(uri, this._request)); return this; }; -internals.Direct.prototype.ttl = function (ttlMsec, isOverride) { // isOverride defaults to true +internals.ttl = function (ttl) { - this.ttlMsec = (isOverride === false ? (this.ttlMsec ? this.ttlMsec : ttlMsec) : ttlMsec); + this._ttl = ttl; return this; }; diff --git a/lib/response/empty.js b/lib/response/empty.js index ff4a26d18..6fdfd2598 100755 --- a/lib/response/empty.js +++ b/lib/response/empty.js @@ -16,7 +16,7 @@ exports = module.exports = internals.Empty = function () { Generic.call(this); this._tag = 'empty'; - this.payload = ''; + this._payload = ''; this.headers['Content-Length'] = 0; return this; diff --git a/lib/response/error.js b/lib/response/error.js index 4f08af39a..66408a215 100755 --- a/lib/response/error.js +++ b/lib/response/error.js @@ -18,7 +18,7 @@ exports = module.exports = internals.Error = function (options) { Obj.call(this, options.payload); this._tag = 'error'; - this.code = options.code; + this._code = options.code; Utils.merge(this.headers, options.headers); if (options.type) { diff --git a/lib/response/generic.js b/lib/response/generic.js index 308d6db88..b4dca4457 100755 --- a/lib/response/generic.js +++ b/lib/response/generic.js @@ -2,6 +2,7 @@ var NodeUtil = require('util'); var Base = require('./base'); +var Utils = require('../utils'); // Declare internals @@ -13,13 +14,14 @@ var internals = {}; exports = module.exports = internals.Generic = function () { + Utils.assert(this.constructor !== internals.Generic, 'Generic must not be instantiated directly'); + Base.call(this); this._tag = 'generic'; - this.code = 200; + this._code = 200; + this._payload = null; this.headers = {}; - this.payload = null; - this.options = {}; return this; }; @@ -29,8 +31,8 @@ NodeUtil.inherits(internals.Generic, Base); internals.Generic.prototype._transmit = function (request, callback) { - request.raw.res.writeHead(this.code, this.headers); - request.raw.res.end(request.method !== 'head' ? this.payload : ''); + request.raw.res.writeHead(this._code, this.headers); + request.raw.res.end(request.method !== 'head' ? this._payload : ''); return callback(); }; @@ -59,15 +61,15 @@ internals.Generic.prototype.bytes = function (bytes) { internals.Generic.prototype.created = function (uri) { - this.code = 201; + this._code = 201; this.headers['Location'] = uri; return this; }; -internals.Generic.prototype.ttl = function (ttlMsec, isOverride) { // isOverride defaults to true +internals.Generic.prototype.ttl = function (ttl, isOverride) { // isOverride defaults to true - this.ttlMsec = (isOverride === false ? (this.ttlMsec ? this.ttlMsec : ttlMsec) : ttlMsec); + this._ttl = (isOverride === false ? (this._ttl ? this._ttl : ttl) : ttl); return this; }; diff --git a/lib/response/headers.js b/lib/response/headers.js new file mode 100755 index 000000000..32d2af314 --- /dev/null +++ b/lib/response/headers.js @@ -0,0 +1,46 @@ +// Load modules + + +// Declare internals + +var internals = {}; + + +exports.set = function (response, request) { + + // Normalize Location header + + if (response.headers && + response.headers.Location) { + + response.headers.Location = exports.location(response.headers.Location, request); + } + + if (response.header && + typeof response.header === 'function') { + // Caching headers + + var isClientCached = (request._route && request._route.cache.isMode('client')); + response.header('Cache-Control', (isClientCached && response._ttl) ? 'max-age=' + Math.floor(response._ttl / 1000) + ', must-revalidate' : 'no-cache'); + + // CORS headers + + if (request.server.settings.cors && + (!request._route || request._route.config.cors !== false)) { + + response.header('Access-Control-Allow-Origin', request.server.settings.cors._origin); + response.header('Access-Control-Max-Age', request.server.settings.cors.maxAge); + response.header('Access-Control-Allow-Methods', request.server.settings.cors._methods); + response.header('Access-Control-Allow-Headers', request.server.settings.cors._headers); + } + } +}; + + +exports.location = function (uri, request) { + + var isAbsolute = (uri.indexOf('http://') === 0 || uri.indexOf('https://') === 0); + return (isAbsolute ? uri : request.server.settings.uri + (uri.charAt(0) === '/' ? '' : '/') + uri); +}; + + diff --git a/lib/response/index.js b/lib/response/index.js index 1997c3840..c8a7d14f0 100755 --- a/lib/response/index.js +++ b/lib/response/index.js @@ -4,6 +4,7 @@ var Stream = require('stream'); var Shot = require('shot'); var Utils = require('../utils'); var Err = require('../error'); +var Headers = require('./headers'); // Declare internals @@ -72,88 +73,57 @@ exports.generateResponse = function (result, onSend) { }; -exports._respond = function (response, request, callback) { +exports._respond = function (item, request, callback) { - var prepareResponse = function(response, send) { + var prepare = function (response) { - if (response && typeof response._prepare === 'function') { - response._prepare(send); - } - else { - send(response); - } - }; - - var handleErrors = function(preparedResponse) { - - if (!preparedResponse || - (!(preparedResponse instanceof internals.Base) && !(preparedResponse instanceof Err))) { + if (!response || + (!(response instanceof internals.Base) && !(response instanceof Err))) { - preparedResponse = Err.internal('Unexpected response object', preparedResponse); + response = Err.internal('Unexpected response item', response); } - // Error object - - if (preparedResponse instanceof Err) { - - var errOptions = (request.server.settings.errors && request.server.settings.errors.format - ? request.server.settings.errors.format(preparedResponse) - : preparedResponse.toResponse()); + if (response._prepare && + typeof response._prepare === 'function') { - request.log(['http', 'response', 'error'], preparedResponse); - preparedResponse = new internals.Error(errOptions); + return response._prepare(send); } - return preparedResponse; + return send(response); }; - var formatHeaders = function(preparedResponse) { + var send = function (response) { - // Normalize Location header - - if (preparedResponse.headers.Location) { - var uri = preparedResponse.headers.Location; - var isAbsolute = (uri.indexOf('http://') === 0 || uri.indexOf('https://') === 0); - preparedResponse.headers.Location = (isAbsolute ? uri : request.server.settings.uri + (uri.charAt(0) === '/' ? '' : '/') + uri); - } - - // Caching headers - - preparedResponse.header('Cache-Control', preparedResponse.ttlMsec ? 'max-age=' + Math.floor(preparedResponse.ttlMsec / 1000) : 'must-revalidate'); + // Error object - // CORS headers + if (response instanceof Err) { - if (request.server.settings.cors && - (!request._route || request._route.config.cors !== false)) { + var errOptions = (request.server.settings.errors && request.server.settings.errors.format + ? request.server.settings.errors.format(response) + : response.toResponse()); - preparedResponse.header('Access-Control-Allow-Origin', request.server.settings.cors._origin); - preparedResponse.header('Access-Control-Max-Age', request.server.settings.cors.maxAge); - preparedResponse.header('Access-Control-Allow-Methods', request.server.settings.cors._methods); - preparedResponse.header('Access-Control-Allow-Headers', request.server.settings.cors._headers); + request.log(['http', 'response', 'error'], response); + response = new internals.Error(errOptions); } - return preparedResponse; - }; + // Set Cache, CORS, Location headers - var transmit = function(preparedResponse, callback) { + Headers.set(response, request); // Injection - if (preparedResponse.payload !== undefined) { // Value can be falsey + if (response._payload !== undefined) { // Value can be falsey if (Shot.isInjection(request.raw.req)) { - request.raw.res.hapi = { result: preparedResponse.raw || preparedResponse.payload }; + request.raw.res.hapi = { result: response.raw || response._payload }; } } - preparedResponse._transmit(request, function () { + response._transmit(request, function () { - request.log(['http', 'response', preparedResponse._tag]); + request.log(['http', 'response', response._tag]); return callback(); }); }; - prepareResponse(response, function(preparedResponse) { - - transmit(formatHeaders(handleErrors(preparedResponse)), callback); - }); + prepare(item); }; diff --git a/lib/response/obj.js b/lib/response/obj.js index 5aee07f5f..48abf92b6 100755 --- a/lib/response/obj.js +++ b/lib/response/obj.js @@ -16,10 +16,10 @@ exports = module.exports = internals.Obj = function (object, type) { Generic.call(this); this._tag = 'obj'; - this.payload = JSON.stringify(object); // Convert immediately to snapshot content + this._payload = JSON.stringify(object); // Convert immediately to snapshot content this.raw = object; // Can change is reference is modified this.headers['Content-Type'] = type || 'application/json'; - this.headers['Content-Length'] = Buffer.byteLength(this.payload); + this.headers['Content-Length'] = Buffer.byteLength(this._payload); return this; }; diff --git a/lib/response/stream.js b/lib/response/stream.js index 9938ba9ba..123ef107a 100755 --- a/lib/response/stream.js +++ b/lib/response/stream.js @@ -17,7 +17,7 @@ exports = module.exports = internals.Stream = function (stream) { Generic.call(this); this._tag = 'stream'; - delete this.payload; + delete this._payload; this.stream = stream; @@ -43,9 +43,9 @@ internals.Stream.prototype._transmit = function (request, callback) { } } - this.code = this.stream.statusCode || ((this.stream.response && this.stream.response.code) ? this.stream.response.code : this.code); + this._code = this.stream.statusCode || ((this.stream.response && this.stream.response.code) ? this.stream.response.code : this._code); - request.raw.res.writeHead(this.code, this.headers); + request.raw.res.writeHead(this._code, this.headers); var isEnded = false; var end = function () { diff --git a/lib/response/text.js b/lib/response/text.js index 3a97f9ac4..78d975e30 100755 --- a/lib/response/text.js +++ b/lib/response/text.js @@ -16,9 +16,9 @@ exports = module.exports = internals.Text = function (text, type) { Generic.call(this); this._tag = 'text'; - this.payload = text; + this._payload = text; this.headers['Content-Type'] = type || 'text/html'; - this.headers['Content-Length'] = Buffer.byteLength(this.payload); + this.headers['Content-Length'] = Buffer.byteLength(this._payload); return this; }; diff --git a/test/integration/cache.js b/test/integration/cache.js index 765d03b0d..2de2707ab 100755 --- a/test/integration/cache.js +++ b/test/integration/cache.js @@ -83,7 +83,7 @@ describe('Cache', function() { it('returns max-age value when route uses default cache rules', function(done) { makeRequest('/profile', function(rawRes) { var headers = parseHeaders(rawRes.raw.res); - expect(headers['Cache-Control']).to.equal('max-age=120'); + expect(headers['Cache-Control']).to.equal('max-age=120, must-revalidate'); done(); }); }); @@ -91,7 +91,7 @@ describe('Cache', function() { it('returns max-age value when route uses client cache mode', function(done) { makeRequest('/profile', function(rawRes) { var headers = parseHeaders(rawRes.raw.res); - expect(headers['Cache-Control']).to.equal('max-age=120'); + expect(headers['Cache-Control']).to.equal('max-age=120, must-revalidate'); done(); }); }); @@ -99,7 +99,7 @@ describe('Cache', function() { it('doesn\'t return max-age value when route is not cached', function(done) { makeRequest('/item2', function(rawRes) { var headers = parseHeaders(rawRes.raw.res); - expect(headers['Cache-Control']).to.not.equal('max-age=120'); + expect(headers['Cache-Control']).to.not.equal('max-age=120, must-revalidate'); done(); }); }); diff --git a/test/integration/response.js b/test/integration/response.js index dd9b2c8ba..369fec7cc 100755 --- a/test/integration/response.js +++ b/test/integration/response.js @@ -27,9 +27,16 @@ describe('Response', function () { request.reply(); }; - var baseHandler = function (request) { + var directHandler = function (request) { - request.reply(new Hapi.Response.Text('hola')); + var response = new Hapi.Response.Direct(request) + .code(200) + .type('text/plain') + .bytes(13) + .write('!hola ') + .write('amigos!'); + + request.reply(response); }; var fileHandler = function(request) { @@ -112,7 +119,7 @@ describe('Response', function () { { method: 'POST', path: '/text', handler: textHandler }, { method: 'POST', path: '/error', handler: errorHandler }, { method: 'POST', path: '/empty', handler: emptyHandler }, - { method: 'POST', path: '/base', handler: baseHandler }, + { method: 'POST', path: '/direct', handler: directHandler }, { method: 'POST', path: '/exp', handler: expHandler }, { method: 'POST', path: '/stream/{issue?}', handler: streamHandler }, { method: 'POST', path: '/file', handler: fileHandler }, @@ -157,14 +164,13 @@ describe('Response', function () { }); }); - it('returns a base reply', function (done) { + it('returns a direct reply', function (done) { - var request = { method: 'POST', url: '/base' }; + var request = { method: 'POST', url: '/direct' }; server.inject(request, function (res) { - expect(res.result).to.exist; - expect(res.result).to.equal('hola'); + expect(res.readPayload()).to.equal('!hola amigos!'); done(); }); }); From fd2b6eb1a2be7d1e8223fe982866d762ee993110 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Sat, 17 Nov 2012 00:45:22 -0800 Subject: [PATCH 3/8] Fix response validation tests --- Readme.md | 7 +- examples/docs/server.js | 2 +- lib/defaults.js | 9 +-- lib/request.js | 163 ++++++++++++++++++++------------------ lib/response/index.js | 18 +++-- lib/response/obj.js | 4 +- lib/validation.js | 10 +-- test/integration/batch.js | 88 ++++++++++++-------- test/unit/validation.js | 28 +++---- 9 files changed, 176 insertions(+), 153 deletions(-) diff --git a/Readme.md b/Readme.md index 7fde45a43..223206b5d 100755 --- a/Readme.md +++ b/Readme.md @@ -167,7 +167,7 @@ during request processing. The required extension function signature is _functio The extension points are: - `onRequest` - called upon new requests before any router processing. The _'request'_ object passed to the `onRequest` functions is decorated with the _'setUrl(url)'_ and _'setMethod(verb)' methods. Calls to these methods will impact how the request is router and can be used for rewrite rules. - `onPreHandler` - called after request passes validation and body parsing, before the request handler. -- `onPostHandler` - called after the request handler, before sending the response. +- `onPostHandler` - called after the request handler, before sending the response. The actual state of the response depends on the response type used (e.g. direct, stream). - `onPostRoute` - called after the response was sent. - `onUnknownRoute` - if defined, overrides the default unknown resource (404) error response. The method must send the response manually via _request.raw.res_. Cannot be an array. @@ -512,16 +512,15 @@ When the provided route handler method is called, it receives a _request_ object - _'session'_ - available for authenticated requests and includes: - _'used'_ - user id. - _'app'_ - application id. - - _'tos'_ - terms-of-service version. - _'scope'_ - approved application scopes. + - _'ext.tos'_ - terms-of-service version. - _'server'_ - a reference to the server object. - _'addTail([name])'_ - adds a request tail as described in [Request Tails](#request-tails). - _'raw'_ - an object containing the Node HTTP server 'req' and 'req' objects. **Direct interaction with these raw objects is not recommended.** -- _'response'_ - contains the route handler's response after the handler is called. **Direct interaction with this raw objects is not recommended.** The request object is also decorated with a _'reply'_ property which includes the following methods: - _'send([result])'_ - replies to the resource request with result - an object (sent as JSON), a string (sent as HTML), or an error generated using the 'Hapi.error' module described in [Errors](#errors). If no result is provided, an empty response body is sent. Calling _'send([result])'_ returns control over to the router. -- _'pipe(stream)'_ - pipes the content of the stream into the response. Calling _'pipe([stream])'_ returns control over to the router. +- _'stream(stream)'_ - pipes the content of the stream into the response. Calling _'pipe([stream])'_ returns control over to the router. - _'created(location)`_ - a URI value which sets the HTTP response code to 201 (Created) and adds the HTTP _Location_ header with the provided value (normalized to absolute URI). - _'bytes(length)'_ - a pre-calculated Content-Length header value. Only available when using _'pipe(stream)'_. - _'type(mimeType)'_ - a pre-determined Content-Type header value. Should only be used to override the built-in defaults. diff --git a/examples/docs/server.js b/examples/docs/server.js index 0b87de63b..6db79069e 100755 --- a/examples/docs/server.js +++ b/examples/docs/server.js @@ -28,7 +28,7 @@ var internals = {}; internals.main = function () { - var config = { name: 'Example', docs: true, debug: true }; + var config = { docs: true, debug: true }; // Create Hapi servers var http = new Hapi.Server('0.0.0.0', 8080, config); diff --git a/lib/defaults.js b/lib/defaults.js index 1eec18e30..37a90246a 100755 --- a/lib/defaults.js +++ b/lib/defaults.js @@ -12,8 +12,6 @@ var internals = {}; exports.server = { - name: '', // Defaults to 'host:port' - // TLS // tls: { @@ -73,10 +71,11 @@ exports.server = { onUnknownRoute: null // Overrides hapi's default handler for unknown route. Cannot be an array! }, - // Errors + // Response formatter - errors: { - format: null // function (result, callback) { callback(formatted_error); } - Overrides the built-in error response format (object or html string) + format: { + error: null, // function (error) { return { code, payload, type, headers }; } + payload: null // function (payload) { return formattedPayload; } }, // Optional components diff --git a/lib/request.js b/lib/request.js index d6194e54c..098378e3d 100755 --- a/lib/request.js +++ b/lib/request.js @@ -52,13 +52,12 @@ exports = module.exports = internals.Request = function (server, req, res, optio // setUrl() // setMethod() - // reply(): { pipe() } + // reply(): { payload(), stream() } // close() // Semi-public members this.pre = {}; - this.response = null; this.raw = { req: req, @@ -70,6 +69,7 @@ exports = module.exports = internals.Request = function (server, req, res, optio // Private members this._route = null; + this._response = null; this._isResponded = false; this._log = []; this._analytics = { @@ -237,8 +237,8 @@ internals.Request.prototype._execute = function (route) { function (err) { if (err) { - if (self.response && - !(self.response instanceof Error)) { + if (self._response && + !(self._response instanceof Error)) { // Got error after valid result was already set @@ -250,7 +250,7 @@ internals.Request.prototype._execute = function (route) { }); } - self.response = err; + self._response = err; } self._respond(); @@ -298,6 +298,19 @@ internals.Request.prototype._decorate = function (callback) { var response = null; + // Utilities + + var setResponse = function (result, process) { + + if (!(result instanceof Stream) && + self.server.settings.format.payload) { + + result = self.server.settings.format.payload(result); + } + + response = Response.generate(result, process); + }; + // Chain finalizers var process = function () { @@ -309,7 +322,7 @@ internals.Request.prototype._decorate = function (callback) { this.reply = function (result) { Utils.assert(!(result instanceof Stream) || !self._route || !self._route.cache.isMode('server'), 'Cannot reply using a stream when caching enabled'); - response = Response.generateResponse(result); + setResponse(result); process(); }; @@ -318,14 +331,14 @@ internals.Request.prototype._decorate = function (callback) { this.reply.stream = function (stream) { Utils.assert(stream instanceof Stream, 'request.reply.stream() requires a stream'); - response = Response.generateResponse(stream, process); + setResponse(stream, process); return response; }; this.reply.payload = function (result) { Utils.assert(!(result instanceof Stream), 'Must use request.reply.stream() with a Stream object'); - response = Response.generateResponse(result, process); + setResponse(result, process); return response; }; }; @@ -345,7 +358,7 @@ internals.Request.prototype._unknown = function () { // No extension handler - this.response = Err.notFound('No such path or method ' + this.path); + this._response = Err.notFound('No such path or method ' + this.path); this._respond(); this._wagTail(); return; @@ -357,7 +370,7 @@ internals.Request.prototype._unknown = function () { // Called when reply...send() or reply() is called - self.response = response; + self._response = response; self._respond(); self._wagTail(); }); @@ -384,6 +397,69 @@ internals.Request.prototype._unknown = function () { }; +internals.Request.prototype._prerequisites = function (next) { + + var self = this; + + if (this._route.prerequisites.length === 0) { + return next(); + } + + /* + { + method: function (request, next) {}, + assign: key, + mode: parallel + } + */ + + var parallelFuncs = []; + var serialFuncs = []; + + var fetch = function (pre) { + + return function (callback) { + + var timer = new Utils.Timer(); + pre.method(self, function (result) { + + if (result instanceof Error) { + self.log(['prerequisites', 'error'], { msec: timer.elapsed(), assign: pre.assign, mode: pre.mode, error: result }); + return callback(result); + } + + self.log(['prerequisites'], { msec: timer.elapsed(), assign: pre.assign, mode: pre.mode }); + if (pre.assign) { + self.pre[pre.assign] = result; + } + callback(); + }); + }; + }; + + for (var i = 0, il = self._route.prerequisites.length; i < il; ++i) { + + var pre = self._route.prerequisites[i]; + var list = (pre.mode === 'parallel' ? parallelFuncs : serialFuncs); + list.push(fetch(pre)); + } + + Async.series([ + function (callback) { + + Async.parallel(parallelFuncs, callback); + }, + function (callback) { + + Async.series(serialFuncs, callback); + } + ], function (err, results) { + + return next(err); + }); +}; + + internals.handler = function (request, next) { var lookup = function () { @@ -408,7 +484,7 @@ internals.handler = function (request, next) { // Store response - request.response = response; + request._response = response; return next(); }); }; @@ -449,74 +525,11 @@ internals.handler = function (request, next) { }; -internals.Request.prototype._prerequisites = function (next) { - - var self = this; - - if (this._route.prerequisites.length === 0) { - return next(); - } - - /* - { - method: function (request, next) {}, - assign: key, - mode: parallel - } - */ - - var parallelFuncs = []; - var serialFuncs = []; - - var fetch = function (pre) { - - return function (callback) { - - var timer = new Utils.Timer(); - pre.method(self, function (result) { - - if (result instanceof Error) { - self.log(['prerequisites', 'error'], { msec: timer.elapsed(), assign: pre.assign, mode: pre.mode, error: result }); - return callback(result); - } - - self.log(['prerequisites'], { msec: timer.elapsed(), assign: pre.assign, mode: pre.mode }); - if (pre.assign) { - self.pre[pre.assign] = result; - } - callback(); - }); - }; - }; - - for (var i = 0, il = self._route.prerequisites.length; i < il; ++i) { - - var pre = self._route.prerequisites[i]; - var list = (pre.mode === 'parallel' ? parallelFuncs : serialFuncs); - list.push(fetch(pre)); - } - - Async.series([ - function (callback) { - - Async.parallel(parallelFuncs, callback); - }, - function (callback) { - - Async.series(serialFuncs, callback); - } - ], function (err, results) { - - return next(err); - }); -}; - - internals.Request.prototype._respond = function () { var self = this; - Response._respond(this.response, this, function () { + Response._respond(this._response, this, function () { if (!self._isResponded) { self.server.emit('response', self); diff --git a/lib/response/index.js b/lib/response/index.js index c8a7d14f0..d59429c68 100755 --- a/lib/response/index.js +++ b/lib/response/index.js @@ -12,22 +12,28 @@ var Headers = require('./headers'); var internals = {}; -// Response types +// Prototype response types exports.Base = internals.Base = require('./base'); exports.Generic = internals.Generic = require('./generic'); + +// Basic response types + exports.Empty = internals.Empty = require('./empty'); exports.Obj = internals.Obj = require('./obj'); exports.Text = internals.Text = require('./text'); exports.Stream = internals.Stream = require('./stream'); exports.File = internals.File = require('./file'); exports.Direct = internals.Direct = require('./direct'); + +// Internal response types + internals.Error = require('./error'); // Utilities -exports.generateResponse = function (result, onSend) { +exports.generate = function (result, onSend) { var response = null; @@ -97,11 +103,7 @@ exports._respond = function (item, request, callback) { // Error object if (response instanceof Err) { - - var errOptions = (request.server.settings.errors && request.server.settings.errors.format - ? request.server.settings.errors.format(response) - : response.toResponse()); - + var errOptions = (request.server.settings.format.error ? request.server.settings.format.error(response) : response.toResponse()); request.log(['http', 'response', 'error'], response); response = new internals.Error(errOptions); } @@ -114,7 +116,7 @@ exports._respond = function (item, request, callback) { if (response._payload !== undefined) { // Value can be falsey if (Shot.isInjection(request.raw.req)) { - request.raw.res.hapi = { result: response.raw || response._payload }; + request.raw.res.hapi = { result: response._raw || response._payload }; } } diff --git a/lib/response/obj.js b/lib/response/obj.js index 48abf92b6..71e8a06dd 100755 --- a/lib/response/obj.js +++ b/lib/response/obj.js @@ -16,8 +16,8 @@ exports = module.exports = internals.Obj = function (object, type) { Generic.call(this); this._tag = 'obj'; - this._payload = JSON.stringify(object); // Convert immediately to snapshot content - this.raw = object; // Can change is reference is modified + this._payload = JSON.stringify(object); // Convert immediately to snapshot content + this._raw = object; // Can change if reference is modified this.headers['Content-Type'] = type || 'application/json'; this.headers['Content-Length'] = Buffer.byteLength(this._payload); diff --git a/lib/validation.js b/lib/validation.js index d7e703837..472e421f4 100755 --- a/lib/validation.js +++ b/lib/validation.js @@ -2,6 +2,7 @@ var Joi = require('joi'); var Err = require('./error'); +var Response = require('./response'); // Declare internals @@ -90,16 +91,11 @@ exports.response = function (request, next) { return next(); } - if (typeof request.response.result !== 'object') { + if (!(request._response instanceof Response.Obj)) { return next(Err.internal('Cannot validate non-object response')); } - if (request.response.result instanceof Error) { - // Do not validate errors - return next(); - } - - Joi.validate(request.response.result, request._route.config.response, function (err) { + Joi.validate(request._response._raw, request._route.config.response, function (err) { next(err ? Err.internal(err.message) : null); }); diff --git a/test/integration/batch.js b/test/integration/batch.js index 36d707e38..a40db9038 100755 --- a/test/integration/batch.js +++ b/test/integration/batch.js @@ -5,11 +5,14 @@ var Sinon = require('sinon'); var Async = require('async'); var Hapi = process.env.TEST_COV ? require('../../lib-cov/hapi') : require('../../lib/hapi'); -describe('Batch', function() { + +describe('Batch', function () { + var _server = null; var _serverUrl = 'http://127.0.0.1:18084'; var profileHandler = function (request) { + request.reply({ 'id': 'fa0dbda9b1b', 'name': 'John Doe' @@ -17,6 +20,7 @@ describe('Batch', function() { }; var activeItemHandler = function (request) { + request.reply({ 'id': '55cf687663', 'name': 'Active Item' @@ -24,6 +28,7 @@ describe('Batch', function() { }; var itemHandler = function (request) { + request.reply({ 'id': request.params.id, 'name': 'Item' @@ -31,36 +36,40 @@ describe('Batch', function() { }; var fetch1 = function (request, next) { + next('Hello'); }; - var fetch2 = function (request, next) { + next(request.pre.m1 + request.pre.m3 + request.pre.m4); }; - var fetch3 = function (request, next) { + process.nextTick(function () { + next(' '); }); }; - var fetch4 = function (request, next) { + next('World'); }; - var fetch5 = function (request, next) { + next(request.pre.m2 + '!'); }; var getFetch = function (request) { + request.reply(request.pre.m5 + '\n'); }; function setupServer(done) { + _server = new Hapi.Server('0.0.0.0', 18084, { batch: true }); _server.addRoutes([ { method: 'GET', path: '/profile', config: { handler: profileHandler } }, @@ -81,14 +90,17 @@ describe('Batch', function() { } } ]); - _server.listener.on('listening', function() { + _server.listener.on('listening', function () { + done(); }); _server.start(); } function makeRequest(payload, callback) { - var next = function(res) { + + var next = function (res) { + return callback(res.result); }; @@ -101,27 +113,32 @@ describe('Batch', function() { before(setupServer); - it('shows single response when making request for single endpoint', function(done) { - makeRequest('{ "requests": [{ "method": "get", "path": "/profile" }] }', function(res) { - expect(res[0].id).to.equal("fa0dbda9b1b"); - expect(res[0].name).to.equal("John Doe"); + it('shows single response when making request for single endpoint', function (done) { + + makeRequest('{ "requests": [{ "method": "get", "path": "/profile" }] }', function (res) { + + expect(res[0].id).to.equal('fa0dbda9b1b'); + expect(res[0].name).to.equal('John Doe'); expect(res.length).to.equal(1); done(); }); }); - it('shows two ordered responses when requesting two endpoints', function(done) { - makeRequest('{ "requests": [{"method": "get", "path": "/profile"}, {"method": "get", "path": "/item"}] }', function(res) { - expect(res[0].id).to.equal("fa0dbda9b1b"); - expect(res[0].name).to.equal("John Doe"); + it('shows two ordered responses when requesting two endpoints', function (done) { + + makeRequest('{ "requests": [{"method": "get", "path": "/profile"}, {"method": "get", "path": "/item"}] }', function (res) { + + expect(res[0].id).to.equal('fa0dbda9b1b'); + expect(res[0].name).to.equal('John Doe'); expect(res.length).to.equal(2); - expect(res[1].id).to.equal("55cf687663"); - expect(res[1].name).to.equal("Active Item"); + expect(res[1].id).to.equal('55cf687663'); + expect(res[1].name).to.equal('Active Item'); done(); }); }); - it('handles a large number of batch requests in parallel', function(done) { + it('handles a large number of batch requests in parallel', function (done) { + var requestBody = '{ "requests": [{"method": "get", "path": "/profile"},' + '{"method": "get", "path": "/item"},' + '{"method": "get", "path": "/profile"},' + @@ -205,33 +222,38 @@ describe('Batch', function() { '] }'; var asyncSpy = Sinon.spy(Async, 'parallel'); - makeRequest(requestBody, function(res) { - expect(res[0].id).to.equal("fa0dbda9b1b"); - expect(res[0].name).to.equal("John Doe"); + makeRequest(requestBody, function (res) { + + expect(res[0].id).to.equal('fa0dbda9b1b'); + expect(res[0].name).to.equal('John Doe'); expect(res.length).to.equal(80); - expect(res[1].id).to.equal("55cf687663"); - expect(res[1].name).to.equal("Active Item"); + expect(res[1].id).to.equal('55cf687663'); + expect(res[1].name).to.equal('Active Item'); expect(asyncSpy.args[0][0].length).to.equal(80); done(); }); }); - it('supports piping a response into the next request', function(done) { - makeRequest('{ "requests": [ {"method": "get", "path": "/item"}, {"method": "get", "path": "/item/$0.id"}] }', function(res) { + it('supports piping a response into the next request', function (done) { + + makeRequest('{ "requests": [ {"method": "get", "path": "/item"}, {"method": "get", "path": "/item/$0.id"}] }', function (res) { + expect(res.length).to.equal(2); - expect(res[0].id).to.equal("55cf687663"); - expect(res[0].name).to.equal("Active Item"); - expect(res[1].id).to.equal("55cf687663"); - expect(res[1].name).to.equal("Item"); + expect(res[0].id).to.equal('55cf687663'); + expect(res[0].name).to.equal('Active Item'); + expect(res[1].id).to.equal('55cf687663'); + expect(res[1].name).to.equal('Item'); done(); }); }); - it('includes errors when they occur in the request', function(done) { - makeRequest('{ "requests": [ {"method": "get", "path": "/item"}, {"method": "get", "path": "/nothere"}] }', function(res) { + it('includes errors when they occur in the request', function (done) { + + makeRequest('{ "requests": [ {"method": "get", "path": "/item"}, {"method": "get", "path": "/nothere"}] }', function (res) { + expect(res.length).to.equal(2); - expect(res[0].id).to.equal("55cf687663"); - expect(res[0].name).to.equal("Active Item"); + expect(res[0].id).to.equal('55cf687663'); + expect(res[0].name).to.equal('Active Item'); expect(res[1].error).to.exist; done(); }); diff --git a/test/unit/validation.js b/test/unit/validation.js index 11546a40d..b5474e817 100755 --- a/test/unit/validation.js +++ b/test/unit/validation.js @@ -2,17 +2,19 @@ var should = require("should"); var qs = require("querystring"); var Validation = process.env.TEST_COV ? require('../../lib-cov/validation') : require('../../lib/validation'); +var Response = process.env.TEST_COV ? require('../../lib-cov/response') : require('../../lib/response'); var Types = require('joi').Types; var S = Types.String, N = Types.Number, O = Types.Object, B = Types.Boolean; + var OhaiHandler = function (hapi, reply) { reply('ohai'); }; -var createRequestObject = function (query, route) { +var createRequestObject = function (query, route, payload) { var qstr = qs.stringify(query); return { @@ -24,6 +26,7 @@ var createRequestObject = function (query, route) { href: route.path + '?' + qstr //'/config?choices=1&choices=2' }, query: query, + payload: payload, path: route.path, method: route.method, _route: { config: route.config }, @@ -55,7 +58,7 @@ describe("Validation", function () { var query = { username: "walmart" }; var request = createRequestObject(query, route); - request.response.result = { username: 'test' }; + request._response = Response.generate({ username: 'test' }); Validation.response(request, function (err) { should.not.exist(err); @@ -76,24 +79,13 @@ describe("Validation", function () { it('should raise an error when validating a non-object response', function(done) { var query = { username: "walmart" }; var request = createRequestObject(query, route); - request.response.result = ''; + request._response = Response.generate('test'); Validation.response(request, function (err) { should.exist(err); done(); }); }); - - it('should not validate an error on the response', function(done) { - var query = { username: "walmart" }; - var request = createRequestObject(query, route); - request.response.result = new Error('test'); - - Validation.response(request, function (err) { - should.not.exist(err); - done(); - }); - }); }); describe('#path', function () { @@ -148,11 +140,11 @@ describe("Validation", function () { }); it('should not raise error on undefined OPTIONAL parameter', function (done) { - var modifiedRoute = { method: 'GET', path: '/', config: { handler: OhaiHandler, validate: { query: { username: S().required(), name: S() } } } }; - var query = { username: "walmart" }; - var request = createRequestObject(query, modifiedRoute); + var modifiedRoute = { method: 'GET', path: '/', config: { handler: OhaiHandler, validate: { schema: { username: S().required(), name: S() } } } }; + var payload = { username: "walmart" }; + var request = createRequestObject({}, modifiedRoute, payload); - Validation.query(request, function (err) { + Validation.payload(request, function (err) { should.not.exist(err); done(); }); From 8305620a3e6b0477ceaf93a3f43febfc4ec3728e Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Sat, 17 Nov 2012 12:45:35 -0800 Subject: [PATCH 4/8] Fixed cached response handling --- lib/cache/stale.js | 12 +++++++-- lib/request.js | 2 ++ lib/response/base.js | 2 -- lib/response/cache.js | 27 ++++++++++++++++++++ lib/response/cacheable.js | 36 +++++++++++++++++++++++++++ lib/response/empty.js | 6 ++--- lib/response/generic.js | 2 -- lib/response/index.js | 2 ++ lib/response/obj.js | 8 +++--- lib/response/text.js | 6 ++--- lib/server.js | 5 +--- test/integration/auth.js | 27 ++++++++++++++++++-- test/integration/response.js | 48 ++++++++++++++++++++++++++---------- 13 files changed, 148 insertions(+), 35 deletions(-) create mode 100755 lib/response/cache.js create mode 100755 lib/response/cacheable.js diff --git a/lib/cache/stale.js b/lib/cache/stale.js index 38f92fd6a..07cee4dab 100755 --- a/lib/cache/stale.js +++ b/lib/cache/stale.js @@ -114,7 +114,15 @@ exports.process = function (cache, key, logFunc, baseTags, generateFunc, callbac // Generate new value - generateFunc(function (err, value, ttl) { + generateFunc(function (err, result, ttl) { + + var value = result; + if (result && + result.toCache && + typeof result.toCache === 'function') { + + value = result.toCache(); + } // Check if already sent stale value @@ -146,7 +154,7 @@ exports.process = function (cache, key, logFunc, baseTags, generateFunc, callbac // Save to cache (lazy) and continue internals.saveToCache(cache, key, log, value, ttl); - return callback(value); + return callback(result); }); }); }; diff --git a/lib/request.js b/lib/request.js index 098378e3d..b2f7f9337 100755 --- a/lib/request.js +++ b/lib/request.js @@ -11,6 +11,7 @@ var Payload = require('./payload'); var Validation = require('./validation'); var Cache = require('./cache'); var Response = require('./response'); +var ResponseCache = require('./response/cache'); // Declare internals @@ -475,6 +476,7 @@ internals.handler = function (request, next) { if (!(response instanceof Error)) { if (cached) { + response = new ResponseCache(response); response.ttl(cached.ttl); } else if (request._route.cache.isMode('client')) { diff --git a/lib/response/base.js b/lib/response/base.js index 01ae09e88..7e0e7660c 100755 --- a/lib/response/base.js +++ b/lib/response/base.js @@ -15,8 +15,6 @@ exports = module.exports = internals.Base = function () { Utils.assert(this.constructor !== internals.Base, 'Base must not be instantiated directly'); this._tag = 'base'; - this.isCacheable = false; - return this; }; diff --git a/lib/response/cache.js b/lib/response/cache.js new file mode 100755 index 000000000..2aaee48ac --- /dev/null +++ b/lib/response/cache.js @@ -0,0 +1,27 @@ +// Load modules + +var NodeUtil = require('util'); +var Generic = require('./generic'); +var Utils = require('../utils'); + + +// Declare internals + +var internals = {}; + + +// Cache response + +exports = module.exports = internals.Cache = function (item) { + + Generic.call(this); + this._tag = 'cache'; + + this._code = item.code; + this._payload = item.payload; + this.headers = item.headers; + + return this; +}; + +NodeUtil.inherits(internals.Cache, Generic); diff --git a/lib/response/cacheable.js b/lib/response/cacheable.js new file mode 100755 index 000000000..774ed1a56 --- /dev/null +++ b/lib/response/cacheable.js @@ -0,0 +1,36 @@ +// Load modules + +var NodeUtil = require('util'); +var Generic = require('./generic'); +var Utils = require('../utils'); + + +// Declare internals + +var internals = {}; + + +// Cacheable response + +exports = module.exports = internals.Cacheable = function (text, type) { + + Utils.assert(this.constructor !== internals.Cacheable, 'Cacheable must not be instantiated directly'); + + Generic.call(this); + this._tag = 'cacheable'; + + return this; +}; + +NodeUtil.inherits(internals.Cacheable, Generic); + + +internals.Cacheable.prototype.toCache = function () { + + return { + code: this._code, + payload: this._payload, + headers: this.headers + }; +}; + diff --git a/lib/response/empty.js b/lib/response/empty.js index 6fdfd2598..a7cb12057 100755 --- a/lib/response/empty.js +++ b/lib/response/empty.js @@ -1,7 +1,7 @@ // Load modules var NodeUtil = require('util'); -var Generic = require('./generic'); +var Cacheable = require('./cacheable'); // Declare internals @@ -13,7 +13,7 @@ var internals = {}; exports = module.exports = internals.Empty = function () { - Generic.call(this); + Cacheable.call(this); this._tag = 'empty'; this._payload = ''; @@ -22,6 +22,6 @@ exports = module.exports = internals.Empty = function () { return this; }; -NodeUtil.inherits(internals.Empty, Generic); +NodeUtil.inherits(internals.Empty, Cacheable); diff --git a/lib/response/generic.js b/lib/response/generic.js index b4dca4457..2744ae3d0 100755 --- a/lib/response/generic.js +++ b/lib/response/generic.js @@ -72,5 +72,3 @@ internals.Generic.prototype.ttl = function (ttl, isOverride) { // isOverrid this._ttl = (isOverride === false ? (this._ttl ? this._ttl : ttl) : ttl); return this; }; - - diff --git a/lib/response/index.js b/lib/response/index.js index d59429c68..0f1bcc1c8 100755 --- a/lib/response/index.js +++ b/lib/response/index.js @@ -16,6 +16,7 @@ var internals = {}; exports.Base = internals.Base = require('./base'); exports.Generic = internals.Generic = require('./generic'); +exports.Cacheable = internals.Cacheable = require('./cacheable'); // Basic response types @@ -29,6 +30,7 @@ exports.Direct = internals.Direct = require('./direct'); // Internal response types internals.Error = require('./error'); +internals.Cache = require('./cache'); // Utilities diff --git a/lib/response/obj.js b/lib/response/obj.js index 71e8a06dd..cf7e0424d 100755 --- a/lib/response/obj.js +++ b/lib/response/obj.js @@ -1,7 +1,7 @@ // Load modules var NodeUtil = require('util'); -var Generic = require('./generic'); +var Cacheable = require('./cacheable'); // Declare internals @@ -13,15 +13,15 @@ var internals = {}; exports = module.exports = internals.Obj = function (object, type) { - Generic.call(this); + Cacheable.call(this); this._tag = 'obj'; this._payload = JSON.stringify(object); // Convert immediately to snapshot content - this._raw = object; // Can change if reference is modified + this._raw = object; // Can change if reference is modified this.headers['Content-Type'] = type || 'application/json'; this.headers['Content-Length'] = Buffer.byteLength(this._payload); return this; }; -NodeUtil.inherits(internals.Obj, Generic); +NodeUtil.inherits(internals.Obj, Cacheable); diff --git a/lib/response/text.js b/lib/response/text.js index 78d975e30..19b4ad6ce 100755 --- a/lib/response/text.js +++ b/lib/response/text.js @@ -1,7 +1,7 @@ // Load modules var NodeUtil = require('util'); -var Generic = require('./generic'); +var Cacheable = require('./cacheable'); // Declare internals @@ -13,7 +13,7 @@ var internals = {}; exports = module.exports = internals.Text = function (text, type) { - Generic.call(this); + Cacheable.call(this); this._tag = 'text'; this._payload = text; @@ -23,4 +23,4 @@ exports = module.exports = internals.Text = function (text, type) { return this; }; -NodeUtil.inherits(internals.Text, Generic); +NodeUtil.inherits(internals.Text, Cacheable); diff --git a/lib/server.js b/lib/server.js index 9d0b1561b..53f334fb9 100755 --- a/lib/server.js +++ b/lib/server.js @@ -103,7 +103,6 @@ module.exports = internals.Server = function (/* host, port, options */) { // Generate CORS headers if (this.settings.cors) { - this.settings.cors._origin = (this.settings.cors.origin || []).join(' '); this.settings.cors._headers = (this.settings.cors.headers || []).concat(this.settings.cors.additionalHeaders || []).join(', '); this.settings.cors._methods = (this.settings.cors.methods || []).concat(this.settings.cors.additionalMethods || []).join(', '); @@ -128,7 +127,6 @@ module.exports = internals.Server = function (/* host, port, options */) { // Setup debug endpoint if (this.settings.debug) { - this._debugConsole = new Helmet(this.settings.debug); var debugMarkup = this._debugConsole.getMarkup(); this.addRoute({ @@ -151,7 +149,6 @@ module.exports = internals.Server = function (/* host, port, options */) { // Setup docs generator endpoint if (this.settings.docs) { - self.addRoute({ method: 'GET', path: self.settings.docs.docsEndpoint, @@ -374,7 +371,7 @@ internals.Server.prototype.addHelper = function (name, method, options) { var lastArgPos = args.length - 1; var next = args[lastArgPos]; - // Wrap method for Cache.Stale interface 'function (callback) { callback(err, value, ttl); }' + // Wrap method for Cache.Stale interface 'function (callback) { callback(err, value); }' var generateFunc = function (callback) { diff --git a/test/integration/auth.js b/test/integration/auth.js index 1378d0d95..9a88efbce 100755 --- a/test/integration/auth.js +++ b/test/integration/auth.js @@ -44,12 +44,23 @@ describe('Auth', function () { request.reply('Success'); }; + var doubleHandler = function (request) { + + var options = { method: 'POST', url: '/basic', headers: { authorization: basicHeader('john', '12345') }, session: request.session }; + + server.inject(options, function (res) { + + request.reply(res.result); + }); + }; + server.addRoutes([ { method: 'POST', path: '/basic', handler: basicHandler }, { method: 'POST', path: '/basicOptional', handler: basicHandler, config: { auth: { mode: 'optional' } } }, { method: 'POST', path: '/basicScope', handler: basicHandler, config: { auth: { scope: 'x' } } }, - { method: 'POST', path: '/basicTos', handler: basicHandler, config: { auth: { tos: 200 } } } -]); + { method: 'POST', path: '/basicTos', handler: basicHandler, config: { auth: { tos: 200 } } }, + { method: 'POST', path: '/double', handler: doubleHandler }, + ]); var basicHeader = function (username, password) { @@ -70,6 +81,18 @@ describe('Auth', function () { }); }); + it('returns a reply on successful double auth', function (done) { + + var request = { method: 'POST', url: '/double', headers: { authorization: basicHeader('john', '12345') } }; + + server.inject(request, function (res) { + + expect(res.result).to.exist; + expect(res.result).to.equal('Success'); + done(); + }); + }); + it('returns a reply on failed optional auth', function (done) { var request = { method: 'POST', url: '/basicOptional' }; diff --git a/test/integration/response.js b/test/integration/response.js index 369fec7cc..377105ca1 100755 --- a/test/integration/response.js +++ b/test/integration/response.js @@ -10,7 +10,7 @@ var Request = require('request'); describe('Response', function () { - var server = new Hapi.Server('0.0.0.0', 17082); + var server = new Hapi.Server('0.0.0.0', 17082, { cache: { engine: 'memory' } }); var textHandler = function (request) { @@ -39,13 +39,13 @@ describe('Response', function () { request.reply(response); }; - var fileHandler = function(request) { + var fileHandler = function (request) { var file = new Hapi.Response.File(__dirname + '/../../package.json'); request.reply(file); }; - var fileNotFoundHandler = function(request) { + var fileNotFoundHandler = function (request) { var file = new Hapi.Response.File(__dirname + '/../../notHere'); request.reply(file); @@ -96,7 +96,7 @@ describe('Response', function () { this.y = callback; } } - break; + break; default: if (event === 'data') { @@ -115,6 +115,11 @@ describe('Response', function () { request.reply.stream(new FakeStream(request.params.issue)).bytes(request.params.issue ? 0 : 1).send(); }; + var cacheHandler = function (request) { + + request.reply({ status: 'cached' }); + }; + server.addRoutes([ { method: 'POST', path: '/text', handler: textHandler }, { method: 'POST', path: '/error', handler: errorHandler }, @@ -124,7 +129,8 @@ describe('Response', function () { { method: 'POST', path: '/stream/{issue?}', handler: streamHandler }, { method: 'POST', path: '/file', handler: fileHandler }, { method: 'POST', path: '/filenotfound', handler: fileNotFoundHandler }, - { method: 'POST', path: '/staticfile', handler: { file: __dirname + '/../../package.json' } } + { method: 'POST', path: '/staticfile', handler: { file: __dirname + '/../../package.json' } }, + { method: 'GET', path: '/cache', config: { handler: cacheHandler, cache: { expiresIn: 5000 } } } ]); it('returns a text reply', function (done) { @@ -221,13 +227,30 @@ describe('Response', function () { }); }); - describe('#file', function() { + it('returns a cached reply', function (done) { + + var request = { method: 'GET', url: '/cache' }; + + server.inject(request, function (res1) { + + expect(res1.result).to.exist; + expect(res1.result.status).to.equal('cached'); + + server.inject(request, function (res2) { + + expect(res2.readPayload()).to.equal('{"status":"cached"}'); + done(); + }); + }); + }); + + describe('#file', function () { it('returns a file in the response with the correct headers', function (done) { - server.start(function() { + server.start(function () { - Request.post('http://localhost:17082/file', function(err, res, body) { + Request.post('http://localhost:17082/file', function (err, res, body) { expect(err).to.not.exist; expect(body).to.contain('hapi'); @@ -240,9 +263,9 @@ describe('Response', function () { it('returns a 404 when the file is not found', function (done) { - server.start(function() { + server.start(function () { - Request.post('http://localhost:17082/filenotfound', function(err, res) { + Request.post('http://localhost:17082/filenotfound', function (err, res) { expect(err).to.not.exist; expect(res.statusCode).to.equal(404); @@ -251,9 +274,9 @@ describe('Response', function () { }); }); - it('returns a file using the built-in handler config', function(done) { + it('returns a file using the built-in handler config', function (done) { - Request.post('http://localhost:17082/staticfile', function(err, res, body) { + Request.post('http://localhost:17082/staticfile', function (err, res, body) { expect(err).to.not.exist; expect(body).to.contain('hapi'); @@ -263,5 +286,4 @@ describe('Response', function () { }); }); }); - }); \ No newline at end of file From c9d46a904f18a41f4cb56a67db08adcbb156ded5 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Sun, 18 Nov 2012 00:08:50 -0800 Subject: [PATCH 5/8] Cleanup ttl --- lib/cache/stale.js | 12 ++++++++---- lib/request.js | 15 +++++---------- lib/response/base.js | 1 + lib/response/cache.js | 5 +++-- lib/response/cacheable.js | 2 +- lib/response/direct.js | 4 ++-- lib/response/empty.js | 2 +- lib/response/error.js | 2 +- lib/response/file.js | 2 +- lib/response/generic.js | 6 +++--- lib/response/headers.js | 15 +++++++++++++-- lib/response/index.js | 10 ++++++++++ lib/response/obj.js | 2 +- lib/response/stream.js | 2 +- lib/response/text.js | 2 +- lib/server.js | 2 +- 16 files changed, 53 insertions(+), 31 deletions(-) diff --git a/lib/cache/stale.js b/lib/cache/stale.js index 07cee4dab..dd897a967 100755 --- a/lib/cache/stale.js +++ b/lib/cache/stale.js @@ -47,7 +47,9 @@ internals.getCached = function (cache, key, log, callback) { internals.saveToCache = function (cache, key, log, value, ttl) { - if (!cache.isMode('server')) { + if (!cache.isMode('server') || + ttl === 0) { // null or undefined means use policy + return; } @@ -91,7 +93,7 @@ exports.process = function (cache, key, logFunc, baseTags, generateFunc, callbac // Not in cache, or cache stale - var wasCallbackCalled = false; // Track state between stale timeout and generate fresh + var wasCallbackCalled = false; // Track state between stale timeout and generate fresh if (cached && cached.isStale) { @@ -108,8 +110,10 @@ exports.process = function (cache, key, logFunc, baseTags, generateFunc, callbac return callback(cached.item, cached); }; - cached.ttl -= cache.rule.staleTimeout; // Adjust TTL for when the timeout is invoked - setTimeout(timerFunc, cache.rule.staleTimeout); + cached.ttl -= cache.rule.staleTimeout; // Adjust TTL for when the timeout is invoked + if (cached.ttl > 0) { + setTimeout(timerFunc, cache.rule.staleTimeout); + } } // Generate new value diff --git a/lib/request.js b/lib/request.js index b2f7f9337..1777ad9db 100755 --- a/lib/request.js +++ b/lib/request.js @@ -472,16 +472,10 @@ internals.handler = function (request, next) { request._route.cache.getOrGenerate(request.url.path, logFunc, generate, function (response, cached) { // request.url.path contains query - // Set TTL + if (cached && + !(response instanceof Error)) { - if (!(response instanceof Error)) { - if (cached) { - response = new ResponseCache(response); - response.ttl(cached.ttl); - } - else if (request._route.cache.isMode('client')) { - response.ttl(request._route.cache.ttl(), false); // Does not override ttl is already set - } + response = new ResponseCache(response, cached.ttl); } // Store response @@ -514,7 +508,8 @@ internals.handler = function (request, next) { } request.log(['handler', 'result'], { msec: timer.elapsed() }); - return callback(null, response, response._ttl); // Not all response types include _ttl + var ttl = (response instanceof Response.Cacheable ? response.options.ttl : 0); // null/undefined: cache defaults, 0: not cached + return callback(null, response, ttl); }); // Execute handler diff --git a/lib/response/base.js b/lib/response/base.js index 7e0e7660c..064411608 100755 --- a/lib/response/base.js +++ b/lib/response/base.js @@ -14,6 +14,7 @@ exports = module.exports = internals.Base = function () { Utils.assert(this.constructor !== internals.Base, 'Base must not be instantiated directly'); this._tag = 'base'; + this.options = {}; return this; }; diff --git a/lib/response/cache.js b/lib/response/cache.js index 2aaee48ac..a53a2b6d1 100755 --- a/lib/response/cache.js +++ b/lib/response/cache.js @@ -10,9 +10,9 @@ var Utils = require('../utils'); var internals = {}; -// Cache response +// Cache response (Base -> Generic -> Cache) -exports = module.exports = internals.Cache = function (item) { +exports = module.exports = internals.Cache = function (item, ttl) { Generic.call(this); this._tag = 'cache'; @@ -20,6 +20,7 @@ exports = module.exports = internals.Cache = function (item) { this._code = item.code; this._payload = item.payload; this.headers = item.headers; + this.options.ttl = ttl; return this; }; diff --git a/lib/response/cacheable.js b/lib/response/cacheable.js index 774ed1a56..5c935c133 100755 --- a/lib/response/cacheable.js +++ b/lib/response/cacheable.js @@ -10,7 +10,7 @@ var Utils = require('../utils'); var internals = {}; -// Cacheable response +// Cacheable response (Base -> Generic -> Cacheable) exports = module.exports = internals.Cacheable = function (text, type) { diff --git a/lib/response/direct.js b/lib/response/direct.js index f022df8e9..eb5029c2d 100755 --- a/lib/response/direct.js +++ b/lib/response/direct.js @@ -12,7 +12,7 @@ var internals = { }; -// Direct response +// Direct response (Base -> Direct) exports = module.exports = internals.Direct = function (request) { @@ -120,7 +120,7 @@ internals.created = function (uri) { internals.ttl = function (ttl) { - this._ttl = ttl; + this.options.ttl = ttl; return this; }; diff --git a/lib/response/empty.js b/lib/response/empty.js index a7cb12057..fb49be4e5 100755 --- a/lib/response/empty.js +++ b/lib/response/empty.js @@ -9,7 +9,7 @@ var Cacheable = require('./cacheable'); var internals = {}; -// Empty response +// Empty response (Base -> Generic -> Cacheable -> Empty) exports = module.exports = internals.Empty = function () { diff --git a/lib/response/error.js b/lib/response/error.js index 66408a215..787c1f217 100755 --- a/lib/response/error.js +++ b/lib/response/error.js @@ -10,7 +10,7 @@ var Utils = require('../utils'); var internals = {}; -// Error response +// Error response (Base -> Generic -> Cacheable -> Obj -> Error) exports = module.exports = internals.Error = function (options) { diff --git a/lib/response/file.js b/lib/response/file.js index 813c5b251..88e033a51 100755 --- a/lib/response/file.js +++ b/lib/response/file.js @@ -13,7 +13,7 @@ var ErrorResponse = require('./error'); var internals = {}; -// File response +// File response (Base -> Generic -> Stream -> File) exports = module.exports = internals.File = function (filePath) { diff --git a/lib/response/generic.js b/lib/response/generic.js index 2744ae3d0..cee3159a0 100755 --- a/lib/response/generic.js +++ b/lib/response/generic.js @@ -10,7 +10,7 @@ var Utils = require('../utils'); var internals = {}; -// Generic response +// Generic response (Base -> Generic) exports = module.exports = internals.Generic = function () { @@ -67,8 +67,8 @@ internals.Generic.prototype.created = function (uri) { }; -internals.Generic.prototype.ttl = function (ttl, isOverride) { // isOverride defaults to true +internals.Generic.prototype.ttl = function (ttl) { - this._ttl = (isOverride === false ? (this._ttl ? this._ttl : ttl) : ttl); + this.options.ttl = ttl; return this; }; diff --git a/lib/response/headers.js b/lib/response/headers.js index 32d2af314..08bdb5860 100755 --- a/lib/response/headers.js +++ b/lib/response/headers.js @@ -18,10 +18,21 @@ exports.set = function (response, request) { if (response.header && typeof response.header === 'function') { + // Caching headers - var isClientCached = (request._route && request._route.cache.isMode('client')); - response.header('Cache-Control', (isClientCached && response._ttl) ? 'max-age=' + Math.floor(response._ttl / 1000) + ', must-revalidate' : 'no-cache'); + if ((!request._route && response.options.ttl) || // No policy, manually set + (request._route && request._route.cache.isMode('client') && response.options.ttl !== 0)) { // Policy, not manually off + + if (!response.options.ttl) { + response.options.ttl = request._route.cache.ttl(); + } + + response.header('Cache-Control', 'max-age=' + Math.floor(response.options.ttl / 1000) + ', must-revalidate'); + } + else { + response.header('Cache-Control', 'no-cache'); + } // CORS headers diff --git a/lib/response/index.js b/lib/response/index.js index 0f1bcc1c8..bdb5c0e71 100755 --- a/lib/response/index.js +++ b/lib/response/index.js @@ -12,6 +12,16 @@ var Headers = require('./headers'); var internals = {}; +/* + /-- Direct /-- Stream -----|--- File + Base --| | + \-- Generic --|--- Cache /-- Text + | | + \-- Cacheable --|--- Empty + | + \-- Object --|-- Error +*/ + // Prototype response types exports.Base = internals.Base = require('./base'); diff --git a/lib/response/obj.js b/lib/response/obj.js index cf7e0424d..8221a20e0 100755 --- a/lib/response/obj.js +++ b/lib/response/obj.js @@ -9,7 +9,7 @@ var Cacheable = require('./cacheable'); var internals = {}; -// Obj response +// Obj response (Base -> Generic -> Cacheable -> Obj) exports = module.exports = internals.Obj = function (object, type) { diff --git a/lib/response/stream.js b/lib/response/stream.js index 123ef107a..15bcf6d96 100755 --- a/lib/response/stream.js +++ b/lib/response/stream.js @@ -11,7 +11,7 @@ var Utils = require('../utils'); var internals = {}; -// Stream response +// Stream response (Base -> Generic -> Stream) exports = module.exports = internals.Stream = function (stream) { diff --git a/lib/response/text.js b/lib/response/text.js index 19b4ad6ce..ef8cad8e2 100755 --- a/lib/response/text.js +++ b/lib/response/text.js @@ -9,7 +9,7 @@ var Cacheable = require('./cacheable'); var internals = {}; -// Text response +// Text response (Base -> Generic -> Cacheable -> Text) exports = module.exports = internals.Text = function (text, type) { diff --git a/lib/server.js b/lib/server.js index 53f334fb9..d45b367ef 100755 --- a/lib/server.js +++ b/lib/server.js @@ -371,7 +371,7 @@ internals.Server.prototype.addHelper = function (name, method, options) { var lastArgPos = args.length - 1; var next = args[lastArgPos]; - // Wrap method for Cache.Stale interface 'function (callback) { callback(err, value); }' + // Wrap method for Cache.Stale interface 'function (callback) { callback(err, value, ttl); }' var generateFunc = function (callback) { From 0d2884c686a7af284fb92bd86ad7973b7f5a258d Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Sun, 18 Nov 2012 01:00:24 -0800 Subject: [PATCH 6/8] Fix Direct response methods --- lib/response/direct.js | 2 +- test/integration/response.js | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/response/direct.js b/lib/response/direct.js index eb5029c2d..55e47f450 100755 --- a/lib/response/direct.js +++ b/lib/response/direct.js @@ -112,7 +112,7 @@ internals.bytes = function (bytes) { internals.created = function (uri) { - this._code = 201; + this.code(201); this._request.raw.res.setHeader('Location', Headers.location(uri, this._request)); return this; }; diff --git a/test/integration/response.js b/test/integration/response.js index 377105ca1..46513f2bc 100755 --- a/test/integration/response.js +++ b/test/integration/response.js @@ -14,7 +14,7 @@ describe('Response', function () { var textHandler = function (request) { - request.reply.payload('text').type('text/plain').bytes(4).send(); + request.reply.payload('text').type('text/plain').bytes(4).ttl(1000).send(); }; var errorHandler = function (request) { @@ -30,9 +30,10 @@ describe('Response', function () { var directHandler = function (request) { var response = new Hapi.Response.Direct(request) - .code(200) + .created('me') .type('text/plain') .bytes(13) + .ttl(1000) .write('!hola ') .write('amigos!'); @@ -121,10 +122,10 @@ describe('Response', function () { }; server.addRoutes([ - { method: 'POST', path: '/text', handler: textHandler }, + { method: 'GET', path: '/text', config: { handler: textHandler, cache: { mode: 'client', expiresIn: 9999 } } }, { method: 'POST', path: '/error', handler: errorHandler }, { method: 'POST', path: '/empty', handler: emptyHandler }, - { method: 'POST', path: '/direct', handler: directHandler }, + { method: 'GET', path: '/direct', config: { handler: directHandler, cache: { mode: 'client', expiresIn: 9999 } } }, { method: 'POST', path: '/exp', handler: expHandler }, { method: 'POST', path: '/stream/{issue?}', handler: streamHandler }, { method: 'POST', path: '/file', handler: fileHandler }, @@ -135,12 +136,13 @@ describe('Response', function () { it('returns a text reply', function (done) { - var request = { method: 'POST', url: '/text' }; + var request = { method: 'GET', url: '/text' }; server.inject(request, function (res) { expect(res.result).to.exist; expect(res.result).to.equal('text'); + expect(res.headers['Cache-Control']).to.equal('max-age=1, must-revalidate'); done(); }); }); @@ -172,10 +174,12 @@ describe('Response', function () { it('returns a direct reply', function (done) { - var request = { method: 'POST', url: '/direct' }; + var request = { method: 'GET', url: '/direct' }; server.inject(request, function (res) { + expect(res.statusCode).to.equal(201); + expect(res.headers.location).to.equal(server.settings.uri + '/me'); expect(res.readPayload()).to.equal('!hola amigos!'); done(); }); From e9002af8659e83e48b2ca9825b67d8a568f9867b Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Sun, 18 Nov 2012 11:00:14 -0800 Subject: [PATCH 7/8] Document new reply interface --- Readme.md | 170 +++++++++++++++++++++++------------ lib/request.js | 8 ++ lib/response/headers.js | 3 - test/integration/response.js | 18 ++++ 4 files changed, 138 insertions(+), 61 deletions(-) diff --git a/Readme.md b/Readme.md index 223206b5d..2604919b4 100755 --- a/Readme.md +++ b/Readme.md @@ -25,7 +25,9 @@ Current version: **0.9.0** - [Payload](#payload) - [Extensions](#extensions) - [Unknown Route](#unknown-route) - - [Errors](#errors) + - [Format](#format) + - [Error Format](#error-format) + - [Payload Format](#payload-format) - [Monitor](#monitor) - [Authentication](#authentication) - [Cache](#cache) @@ -42,9 +44,10 @@ Current version: **0.9.0** - [Path Processing](#path-processing) - [Parameters](#parameters) - [Route Handler](#route-handler) - - [Request Logging](#request-logging) + - [Response](#response) - [Proxy](#proxy) - - [Static file handler](#file) + - [Files](#files) + - [Request Logging](#request-logging) - [Query Validation](#query-validation) - [Payload Validation](#payload-validation) - [Path Validation](#path-validation) @@ -119,7 +122,7 @@ var server = new Hapi.Server(); - [`router`](#router) - [`payload`](#payload) - [`ext`](#extensions) -- [`errors`](#Errors) +- [`errors`](#errors) - [`monitor`](#monitor) - [`authentication`](#authentication) - [`cache`](#cache) @@ -247,25 +250,59 @@ function onUnknownRoute(request) { } ``` -### Errors +### Format + +The `format` option provides an extension point for use of custom methods to format error responses or payloads before they are sent back to the client. -If a different error format than the default JSON response is required, the server `errors.format` option can be assigned a function to generate a -different error response. The function signature is _'function (result, callback)'_ where: + +#### Error Format + +If a different error format than the default JSON response is required, the server `format.error` option can be assigned a function to generate a +different error response. The function signature is _'formatted = function (result)'_ where: - _'result'_ - is the **hapi** error object returned by the route handler, and -- _'callback'_ - is a callback function called with the formatted response. The callback function signature is _'function (code, payload, contentType)'_. +- _'formatted'_ - is the formatted response object which contains the following keys: + - _`code`_ - the HTTP status code. + - _`payload`_ - the response payload. + - _`type`_ - the response payload content-type. + - _`headers`_ - any additional response HTTP headers (object). + +Note that the format function must be synchronies. For example: ```javascript var options = { - errors: { - format: function (result, callback) { + format: { + error: function (result) { - callback(500, 'Oops: ' + result.message, 'text/html'); + return { code: 500, payload: 'Oops: ' + result.message, type: 'text/html' }; } } }; ``` +#### Payload Format + +In cases where every non-error payload has to be processed before being sent out (e.g. when returning a database object and need to hide certain fields or +rename '_id' to 'id'), the `format.payload' option can be set to a function that is called on every result, immediately after 'request.reply' is called. The +function's signature is _'formatted = function (result)'_ where: +- _'result'_ - is the raw result object returned by the route handler, and +- _'formatted'_ - is the formatted response to replace 'result'. + +Note that the format function must be synchronies, and it is only invoked for response types other than Stream. + +For example: +```javascript +var options = { + format: { + payload: function (result) { + + return 'something else instead'; + } + } +}; +``` + + ### Monitor **hapi** comes with a built-in process monitor for three types of events: @@ -426,7 +463,7 @@ to write additional text as the configuration itself serves as a living document * _'optional'_ - authentication is optional (validated if present). * `tos` - minimum terms-of-service version required. This is compared to the terms-of-service version accepted by the user. Defaults to _none_. * `scope` - required application scope. Defaults to _none_. - * `entity` - the required authenticated entity type. Available options include: + * `entity` - the required authenticated entity type. Not supported with every authorization scheme. Available options include: * _'any'_ - the authentication can be on behalf of a user or application. * _'user'_ - the authentication must be on behalf of a user. * _'app'_ - the authentication must be on behalf of an application. @@ -510,27 +547,80 @@ When the provided route handler method is called, it receives a _request_ object - _'rawBody'_ - the raw request payload (except for requests with `config.payload` set to _'stream'_). - _'payload'_ - an object containing the parsed request payload (for requests with `config.payload` set to _'parse'_). - _'session'_ - available for authenticated requests and includes: - - _'used'_ - user id. - - _'app'_ - application id. - - _'scope'_ - approved application scopes. - - _'ext.tos'_ - terms-of-service version. + - _'id'_ - session identifier. + - _'used'_ - user id (optional). + - _'app'_ - application id (optional). + - _'scope'_ - approved application scopes (optional). + - _'ext.tos'_ - terms-of-service version (optional). - _'server'_ - a reference to the server object. +- _'pre'_ - any requisites as described in [Prequisites](#prequisites). - _'addTail([name])'_ - adds a request tail as described in [Request Tails](#request-tails). - _'raw'_ - an object containing the Node HTTP server 'req' and 'req' objects. **Direct interaction with these raw objects is not recommended.** -The request object is also decorated with a _'reply'_ property which includes the following methods: -- _'send([result])'_ - replies to the resource request with result - an object (sent as JSON), a string (sent as HTML), or an error generated using the 'Hapi.error' module described in [Errors](#errors). If no result is provided, an empty response body is sent. Calling _'send([result])'_ returns control over to the router. -- _'stream(stream)'_ - pipes the content of the stream into the response. Calling _'pipe([stream])'_ returns control over to the router. +#### Response + +**hapi** provides native support for the following response types: +- Empty - an empty response body (content-lenght of zero). +- Text - plain text. Defaults to 'text/html' content-type. +- Obj - Javascript object, converted to string. Defaults to 'application/json' content-type. +- Stream - a stream object, directly piped into the HTTP response. +- File - transmits a static file. Defaults to the matching mime type based on filename extension. +- Direct - special response type for writing directly to the response object. Used for chunked responses. +- Error - error objects generated using the 'Hapi.error' module or 'new Error()' described in [Response Errors](#response-errors). + +The request object includes a _'reply'_ property which includes the following methods: +- _'payload(result)'_ - sets the provided _'result'_ as the response payload. _'result'_ cannot be a Stream. The mehtod will automatically identify the result type and cast it into one of the supported response types (Empty, Text, Obj, or Error). _'result'_ can all be an instance of any other response type provided by the 'Hapi.response' module (e.g. File, Direct). +- _'stream(stream)'_ - pipes the content of the stream into the response. +- _'send()'_ - finalizes the response and return control back to the router. Must be called after _'payload()'_ or _'stream()'_ to send the response. + +For convenience, the 'response' object is also decorated with a shortcut function _'reply([result])'_ which is identical to calling _'reply.payload([result]).send()'_ or _'reply.stream(stream).send()'_. + +The 'payload()' and 'stream()' methods return a **hapi** Response object created based on the result item provided. +Depending on the response type, additional chainable methods are available: - _'created(location)`_ - a URI value which sets the HTTP response code to 201 (Created) and adds the HTTP _Location_ header with the provided value (normalized to absolute URI). - _'bytes(length)'_ - a pre-calculated Content-Length header value. Only available when using _'pipe(stream)'_. - _'type(mimeType)'_ - a pre-determined Content-Type header value. Should only be used to override the built-in defaults. - _'ttl(msec)'_ - a milliseconds value which overrides the default route cache expiration rule for this individual response. -In addition, the _'reply([result])'_ shortcut is provided which is identical to calling _'reply.send([result])'_. - -The handler must call _'reply()'_, _'reply.send()'_, or _'reply.pipe()'_ (and only one, once) to return control over to the router. The helper methods are only available +The handler must call _'reply()'_, _'reply.send()'_, or _'reply.payload/stream()...send()'_ (and only one, once) to return control over to the router. The reply methods are only available within the route handler and are disabled as soon as control is returned. +#### Proxy + +It is possible with hapi to setup a reverse proxy for routes. This is especially useful if you plan to stand-up hapi in front of an existing API or you need to augment the functionality of an existing API. Additionally, this feature is powerful in that it can be combined with caching to cache the responses from external APIs. The proxy route configuration has the following options: +* `passThrough` - determines if the headers sent from the clients user-agent will be forwarded on to the external service being proxied to (default: false) +* `xforward` - determines if the x-forward headers will be set when making a request to the proxied endpoint (default: false) +* `host` - The host to proxy requests to. The same path on the client request will be used as the path to the host. +* `port` - The port to use when making a request to the host. +* `protocol` - The protocol to use when making a request to the proxied host (http or https) +* `mapUri` - A function that receives the clients request and a passes the URI to a callback to make the proxied request to. If host is set mapUri cannot be used, set either host or mapUri. +* `postResponse` - A function that will be executed before sending the response to the client for requests that can be cached. Use this for any custom error handling of responses from the proxied endpoint. + +For example, to proxy a request to the homepage to google: +```javascript +// Create Hapi servers +var http = new Hapi.Server('0.0.0.0', 8080); + +// Proxy request to / to google.com +http.addRoute({ method: 'GET', path: '/', handler: { proxy: { protocol: 'http', host: 'google.com', port: 80 } } }); + +http.start(); +``` + +#### Files + +It is possible with hapi to respond with a file for a given route. This is easy to configure on a route by specifying an object as the handler that has a property of file. The value of file should be the full local path to the file that should be served. Below is an example of this configuration. + +```javascript +// Create Hapi servers +var http = new Hapi.Server('0.0.0.0', 8080); + +// Serve index.html file up a directory in the public folder +http.addRoute({ method: 'GET', path: '/', handler: { file: __dirname + '/../public/index.html' } }); + +http.start(); +``` + #### Request Logging In addition to the [General Events Logging](#general-events-logging) mechanism provided to log non-request-specific events, **hapi** provides @@ -633,42 +723,6 @@ The server-side cache also supports these advanced options: * `staleIn` - number of milliseconds from the time the item was saved in the cache after which it is considered stale. Value must be less than 86400000 milliseconds (one day) if using `expiresAt` or less than the value of `expiresIn`. Used together with `staleTimeout`. * `staleTimeout` - if a cached response is stale (but not expired), the route will call the handler to generate a new response and will wait this number of milliseconds before giving up and using the stale response. When the handler finally completes, the cache is updated with the more recent update. Value must be less than `expiresIn` if used (after adjustment for units). -### Proxy - -It is possible with hapi to setup a reverse proxy for routes. This is especially useful if you plan to stand-up hapi in front of an existing API or you need to augment the functionality of an existing API. Additionally, this feature is powerful in that it can be combined with caching to cache the responses from external APIs. The proxy route configuration has the following options: -* `passThrough` - determines if the headers sent from the clients user-agent will be forwarded on to the external service being proxied to (default: false) -* `xforward` - determines if the x-forward headers will be set when making a request to the proxied endpoint (default: false) -* `host` - The host to proxy requests to. The same path on the client request will be used as the path to the host. -* `port` - The port to use when making a request to the host. -* `protocol` - The protocol to use when making a request to the proxied host (http or https) -* `mapUri` - A function that receives the clients request and a passes the URI to a callback to make the proxied request to. If host is set mapUri cannot be used, set either host or mapUri. -* `postResponse` - A function that will be executed before sending the response to the client for requests that can be cached. Use this for any custom error handling of responses from the proxied endpoint. - -For example, to proxy a request to the homepage to google: -```javascript -// Create Hapi servers -var http = new Hapi.Server('0.0.0.0', 8080); - -// Proxy request to / to google.com -http.addRoute({ method: 'GET', path: '/', handler: { proxy: { protocol: 'http', host: 'google.com', port: 80 } } }); - -http.start(); -``` - -### File - -It is possible with hapi to respond with a file for a given route. This is easy to configure on a route by specifying an object as the handler that has a property of file. The value of file should be the full local path to the file that should be served. Below is an example of this configuration. - -```javascript -// Create Hapi servers -var http = new Hapi.Server('0.0.0.0', 8080); - -// Serve index.html file up a directory in the public folder -http.addRoute({ method: 'GET', path: '/', handler: { file: __dirname + '/../public/index.html' } }); - -http.start(); -``` - ### Prequisites Before the handler is called, it is often necessary to perform other actions such as loading required reference data from a database. The `pre` option diff --git a/lib/request.js b/lib/request.js index 1777ad9db..d9146ac58 100755 --- a/lib/request.js +++ b/lib/request.js @@ -327,6 +327,14 @@ internals.Request.prototype._decorate = function (callback) { process(); }; + this.reply.send = function () { + + if (response === null) { + setResponse(null); + } + process(); + }; + // Chain initializers this.reply.stream = function (stream) { diff --git a/lib/response/headers.js b/lib/response/headers.js index 08bdb5860..0f00bbdb3 100755 --- a/lib/response/headers.js +++ b/lib/response/headers.js @@ -1,6 +1,3 @@ -// Load modules - - // Declare internals var internals = {}; diff --git a/test/integration/response.js b/test/integration/response.js index 46513f2bc..eb3da70a7 100755 --- a/test/integration/response.js +++ b/test/integration/response.js @@ -27,6 +27,11 @@ describe('Response', function () { request.reply(); }; + var emptyLongHandler = function (request) { + + request.reply.send(); + }; + var directHandler = function (request) { var response = new Hapi.Response.Direct(request) @@ -125,6 +130,7 @@ describe('Response', function () { { method: 'GET', path: '/text', config: { handler: textHandler, cache: { mode: 'client', expiresIn: 9999 } } }, { method: 'POST', path: '/error', handler: errorHandler }, { method: 'POST', path: '/empty', handler: emptyHandler }, + { method: 'POST', path: '/emptyLong', handler: emptyLongHandler }, { method: 'GET', path: '/direct', config: { handler: directHandler, cache: { mode: 'client', expiresIn: 9999 } } }, { method: 'POST', path: '/exp', handler: expHandler }, { method: 'POST', path: '/stream/{issue?}', handler: streamHandler }, @@ -172,6 +178,18 @@ describe('Response', function () { }); }); + it('returns an empty reply (long)', function (done) { + + var request = { method: 'POST', url: '/emptyLong' }; + + server.inject(request, function (res) { + + expect(res.result).to.exist; + expect(res.result).to.equal(''); + done(); + }); + }); + it('returns a direct reply', function (done) { var request = { method: 'GET', url: '/direct' }; From 8ff858c4fda2078e498b962f26fa60caf9e29ca0 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Mon, 19 Nov 2012 10:20:22 -0800 Subject: [PATCH 8/8] Typo --- Readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Readme.md b/Readme.md index 2604919b4..f1dca6736 100755 --- a/Readme.md +++ b/Readme.md @@ -266,7 +266,7 @@ different error response. The function signature is _'formatted = function (resu - _`type`_ - the response payload content-type. - _`headers`_ - any additional response HTTP headers (object). -Note that the format function must be synchronies. +Note that the format function must be synchronous. For example: ```javascript @@ -288,7 +288,7 @@ function's signature is _'formatted = function (result)'_ where: - _'result'_ - is the raw result object returned by the route handler, and - _'formatted'_ - is the formatted response to replace 'result'. -Note that the format function must be synchronies, and it is only invoked for response types other than Stream. +Note that the format function must be synchronous, and it is only invoked for response types other than Stream. For example: ```javascript