diff --git a/examples/stream.js b/examples/stream.js index c1112b9c7..f8e6ab216 100755 --- a/examples/stream.js +++ b/examples/stream.js @@ -23,9 +23,10 @@ internals.main = function () { internals.echo = function (request) { - request.reply.type(request.raw.req.headers['Content-Type']) + request.reply.stream(request.raw.req) + .type(request.raw.req.headers['Content-Type']) .bytes(request.raw.req.headers['Content-Length']) - .stream(request.raw.req); + .send(); }; diff --git a/lib/defaults.js b/lib/defaults.js index 3e5c5b468..1eec18e30 100755 --- a/lib/defaults.js +++ b/lib/defaults.js @@ -69,7 +69,7 @@ exports.server = { onPostHandler: null, // After route handler returns, before sending response onPostRoute: null, // After response sent - // function (request) { request.reply.send(result); OR request.reply.close(); } + // function (request) { request.reply(result); OR request.reply.close(); } onUnknownRoute: null // Overrides hapi's default handler for unknown route. Cannot be an array! }, diff --git a/lib/error.js b/lib/error.js index 240cded69..06d54d8fd 100755 --- a/lib/error.js +++ b/lib/error.js @@ -10,17 +10,31 @@ var Utils = require('./utils'); var internals = {}; -exports = module.exports = internals.Error = function (code, message, options) { +exports = module.exports = internals.Error = function (code, message) { Utils.assert(this.constructor === internals.Error, 'Error must be instantiated using new'); - Utils.assert(!options || !options.toResponse || typeof options.toResponse === 'function', 'options.toReponse must be a function'); - Utils.assert(code >= 400, 'Error code must be 4xx or 5xx'); + Utils.assert(code instanceof Error || (!isNaN(parseFloat(code)) && isFinite(code) && code >= 400), 'code must be an Error or a number (400+)'); Error.call(this); - this.code = code; - this.message = message; - this._settings = Utils.clone(options) || {}; // options can be reused; + if (code instanceof Error) { + for (var d in code) { + if (code.hasOwnProperty(d)) { + this[d] = code[d]; + } + } + + this.code = 500; + this.name = code.name; + this.message = code.message || message; + if (code.message && message) { + this.info = message; + } + } + else { + this.code = code; + this.message = message; + } return this; }; @@ -30,10 +44,6 @@ NodeUtil.inherits(internals.Error, Error); internals.Error.prototype.toResponse = function () { - if (this._settings.toResponse) { - return this._settings.toResponse.call(this); - } - var response = { code: this.code, payload: { @@ -41,12 +51,12 @@ internals.Error.prototype.toResponse = function () { code: this.code, message: this.message } - // contentType: 'application/json' }; for (var d in this) { if (this.hasOwnProperty(d) && - !response.payload.hasOwnProperty(d)) { + !response.payload.hasOwnProperty(d) && + typeof this[d] !== 'function') { response.payload[d] = this[d]; } @@ -84,7 +94,11 @@ internals.Error.notFound = function (message) { internals.Error.internal = function (message, data) { - var format = function () { + var err = new internals.Error(500, message); + err.trace = Utils.callStack(1); + err.data = data; + + err.toResponse = function () { var response = { code: 500, @@ -98,63 +112,32 @@ internals.Error.internal = function (message, data) { return response; }; - var err = new internals.Error(500, message, { toResponse: format }); - err.trace = Utils.callStack(1); - err.data = data; return err; }; internals.Error.passThrough = function (code, payload, contentType) { - var format = function () { + var err = new internals.Error(500, 'Pass-through'); // 500 code is only used internally and is not exposed when sent + + err.passThrough = { + code: code, + payload: payload, + type: contentType + }; + + err.toResponse = function () { var response = { code: code, payload: payload, - contentType: contentType + type: contentType }; return response; }; - - var err = new internals.Error(500, 'Pass-through', { toResponse: format }); // 500 code is only used internally and is not exposed when sent - - err.passThrough = { - code: code, - payload: payload, - contentType: contentType - }; return err; }; -internals.Error.toResponse = function (err) { - - Utils.assert(err instanceof Error, 'Input must be instance of Error'); - - if (err.toResponse && - typeof err.toResponse === 'function') { - - return err.toResponse(); - } - - // Other Error - - var response = { - code: 500, - payload: { - message: err.message, - name: err.name - } - }; - - for (var d in err) { - if (err.hasOwnProperty(d)) { - response.payload[d] = err[d]; - } - } - - return response; -}; \ No newline at end of file diff --git a/lib/hapi.js b/lib/hapi.js index a186ff694..52710d5ff 100755 --- a/lib/hapi.js +++ b/lib/hapi.js @@ -5,6 +5,7 @@ var internals = { error: require('./error'), log: require('./log'), server: require('./server'), + response: require('./response'), utils: require('./utils'), types: require('joi').Types } diff --git a/lib/proxy.js b/lib/proxy.js index ee34c97c2..df9a778c0 100755 --- a/lib/proxy.js +++ b/lib/proxy.js @@ -104,7 +104,7 @@ internals.Proxy.prototype.handler = function () { reqStream.on('response', function (resStream) { - request.reply.stream(resStream); // Request._respond will pass-through headers and status code + request.reply(resStream); // Request._respond will pass-through headers and status code }); } }); @@ -134,9 +134,10 @@ internals.postResponse = function (request, settings, response, payload) { return request.reply(Err.passThrough(statusCode, payload, contentType)); } + var response = request.reply.payload(payload); if (contentType) { - request.reply.type(contentType); + response.type(contentType); } - return request.reply(payload); + return response.send(); }; \ No newline at end of file diff --git a/lib/request.js b/lib/request.js index 703fb6b7a..445d92b37 100755 --- a/lib/request.js +++ b/lib/request.js @@ -5,12 +5,12 @@ var Events = require('events'); var Stream = require('stream'); var Url = require('url'); var Async = require('async'); -var Shot = require('shot'); var Utils = require('./utils'); var Err = require('./error'); var Payload = require('./payload'); var Validation = require('./validation'); var Cache = require('./cache'); +var Response = require('./response'); // Declare internals @@ -58,11 +58,7 @@ exports = module.exports = internals.Request = function (server, req, res, optio // Semi-public members this.pre = {}; - this.response = { - result: null, - options: {} // created, contentType, contentLength - // injection - }; + this.response = null; this.raw = { req: req, @@ -230,7 +226,7 @@ internals.Request.prototype._execute = function (route) { Validation.path, ext(this.server.settings.ext.onPreHandler), internals.handler, // Must not call next() with an Error - ext(this.server.settings.ext.onPostHandler), // An error from onPostHandler will override any result set in handler() + ext(this.server.settings.ext.onPostHandler), // An error from here on will override any result set in handler() Validation.response ]; @@ -241,17 +237,20 @@ internals.Request.prototype._execute = function (route) { function (err) { if (err) { - if (self.response.result) { - // Got error after result was already set + if (self.response && + !(self.response instanceof Error)) { + + // Got error after valid result was already set + self._route.cache.drop(self.url.path, function (err) { + if (err) { self.log(['request', 'cache', 'drop', 'error'], { error: err.message }); } }); } - self.response.result = err; - self.response.options = {}; + self.response = err; } self._respond(); @@ -279,74 +278,37 @@ internals.Request.prototype._decorate = function (callback) { var self = this; - var response = { - result: null, - options: {} - }; + var response = null; + + // Chain finalizers - var process = function (result) { + var process = function () { self._undecorate(); - response.result = result; - return callback(response); }; - // Chain finalizers - this.reply = function (result) { - Utils.assert(!(result instanceof Stream), 'Cannot take a stream'); - Utils.assert(response.options.contentLength === undefined, 'Does not support custom content length'); - process(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); + process(); }; - this.reply.send = function (result) { - - self.reply(result); - }; + // Chain initializers - this.reply.stream = function (stream, options) { + this.reply.stream = function (stream) { Utils.assert(stream instanceof Stream, 'request.reply.stream() requires a stream'); - Utils.assert(!self._route || !self._route.cache.isMode('server'), 'Cannot reply using a stream when caching enabled'); - process(stream); - }; - - // Chain properties - - this.reply.created = function (uri) { - - Utils.assert(self.method === 'post' || self.method === 'put', 'Cannot create resource from a non-POST/PUT handler'); - var isAbsolute = (uri.indexOf('http://') === 0 || uri.indexOf('https://') === 0); - response.options.created = (isAbsolute ? uri : self.server.settings.uri + (uri.charAt(0) === '/' ? '' : '/') + uri); - return self.reply; + response = Response.generateResponse(stream, process); + return response; }; - this.reply.type = function (type) { + this.reply.payload = function (result) { - response.options.contentType = type; - return self.reply; - }; - - this.reply.bytes = function (bytes) { - - response.options.contentLength = bytes; - return self.reply; - }; - - this.reply.ttl = function (msec) { - - Utils.assert(self._route.cache.isEnabled(), 'Cannot set ttl when route caching is not enabled'); - response.options.ttl = msec; - return self.reply; - }; - - this.reply.header = function (name, value) { - - response.options.headers = response.options.headers || {}; - response.options.headers[name] = value; - return self.reply; + Utils.assert(!(result instanceof Stream), 'Must use request.reply.stream() with a Stream object'); + response = Response.generateResponse(result, process); + return response; }; }; @@ -365,7 +327,7 @@ internals.Request.prototype._unknown = function () { // No extension handler - this.response.result = 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; @@ -375,11 +337,9 @@ internals.Request.prototype._unknown = function () { this._decorate(function (response) { - // Called when reply.send() or .pipe() is called - - self.response.result = response.result; - self.response.options = response.options; + // Called when reply...send() or reply() is called + self.response = response; self._respond(); self._wagTail(); }); @@ -421,20 +381,20 @@ internals.handler = function (request, next) { request._route.cache.getOrGenerate(request.url.path, logFunc, request._generateResponse(), function (response, cached) { // request.url.path contains query - // Store response - - request.response.result = response.result; - request.response.options = response.options; - // Set TTL - if (cached) { - request.response.options.ttl = cached.ttl; - } - else if (request._route.cache.isMode('client')) { - request.response.options.ttl = request.response.options.ttl || request._route.cache.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 + } } + // Store response + + request.response = response; return next(); }); }; @@ -461,13 +421,13 @@ internals.Request.prototype._generateResponse = function () { // Check for Error result - if (response.result instanceof Error) { + if (response instanceof Error) { self.log(['handler', 'result', 'error'], { msec: timer.elapsed() }); return callback(response); } self.log(['handler', 'result'], { msec: timer.elapsed() }); - return callback(null, response, response.options.ttl); + return callback(null, response, response.ttlMsec); }); // Execute handler @@ -541,202 +501,17 @@ internals.Request.prototype._prerequisites = function (next) { }; -// Set default response headers and send response - -internals.Request.prototype._setCors = function (headers) { - - // Set CORS headers - - if (this.server.settings.cors && - (!this._route || this._route.config.cors !== false)) { - - headers['Access-Control-Allow-Origin'] = this.server.settings.cors._origin; - headers['Access-Control-Max-Age'] = this.server.settings.cors.maxAge; - headers['Access-Control-Allow-Methods'] = this.server.settings.cors._methods; - headers['Access-Control-Allow-Headers'] = this.server.settings.cors._headers; - } -}; - - -// Set cache response headers - -internals.Request.prototype._setCache = function (headers) { - - if (this._route && - this._route.cache.isMode('client') && - this.response.options.ttl && - !(this.response.result instanceof Error)) { - - headers['Cache-Control'] = 'max-age=' + Math.floor(this.response.options.ttl / 1000); // Convert MSec ttl to Sec in HTTP header - } - else { - headers['Cache-Control'] = 'must-revalidate'; - } -}; - - internals.Request.prototype._respond = function () { var self = this; - var code = 200; - var headers = this.response.options.headers || {}; - var payload = null; - - var review = function () { - - // Set CORS and Cache headers - - self._setCors(headers); - self._setCache(headers); - - // Set options - - if (self.response.options.created) { - code = 201; - headers.Location = self.response.options.created; - } - - if (self.response.options.contentType) { - headers['Content-Type'] = self.response.options.contentType; - } - - if (self.response.options.contentLength) { - headers['Content-Length'] = self.response.options.contentLength; - } - - // Empty response - - var result = self.response.result; - if (!result) { - inject(null); - self.log(['http', 'response', 'empty']); - return send(); - } - - // Error response format - - if (result instanceof Error) { - self.log(['http', 'response', 'error'], result); - - if (self.server.settings.errors && - self.server.settings.errors.format) { - - self.server.settings.errors.format(result, function (errCode, errPayload, errContentType) { - - code = errCode; - result = errPayload; - headers['Content-Type'] = errContentType || headers['Content-Type']; // Override - }); - } - else { - var errResponse = (result instanceof Err ? result.toResponse() : Err.toResponse(result)); - code = errResponse.code || 500; - result = errResponse.payload || ''; - headers['Content-Type'] = errResponse.contentType || headers['Content-Type']; // Override - } - } - else { - self.log(['http', 'response']); - } - - // Payload - - if (typeof result === 'object') { - - // Object - - if (result instanceof Stream) { - - // Stream - - payload = result; - self.log(['http', 'response', 'stream']); - return stream(); - } - - // Object - - payload = JSON.stringify(result); - headers['Content-Type'] = headers['Content-Type'] || 'application/json'; // If not defined - } - else { - - // Non-object (String, etc.) - - payload = (typeof result === 'string' ? result : JSON.stringify(result)); - headers['Content-Type'] = headers['Content-Type'] || 'text/html'; // If not defined - } - - // Non-stream - - inject(result); - - if (payload !== null && // payload can be empty string - !headers['Content-Length']) { - - headers['Content-Length'] = Buffer.byteLength(payload); - } - - return send(); - }; - - var inject = function (result) { - - if (Shot.isInjection(self.raw.req)) { - self.raw.res.hapi = { result: result }; - } - }; - - var send = function () { - - self.raw.res.writeHead(code, headers); - self.raw.res.end(self.method !== 'head' ? payload : ''); + Response._respond(this.response, this, function () { if (!self._isResponded) { self.server.emit('response', self); self._isResponded = true; } - }; - - var stream = function () { - - // Check if payload is a node HTTP response (payload.*) or a (mikeal's) Request object (payload.response.*) - - if (!self._route || - !self._route.config.proxy || - self._route.config.proxy.passThrough) { // Pass headers only if not proxy or proxy with pass-through set - - var responseHeaders = payload.response ? payload.response.headers : payload.headers; - if (responseHeaders) { - Utils.merge(headers, responseHeaders); - } - } - - code = payload.statusCode || ((payload.response && payload.response.code) ? payload.response.code : code); - - self.raw.res.writeHead(code, headers); - - self.raw.req.on('close', function () { - - payload.destroy.bind(payload); - }); - - payload.on('error', function () { - - self.raw.req.destroy(); - }); - - payload.on('end', function () { - - self.raw.res.end(); - }); - - payload.resume(); - payload.pipe(self.raw.res); - }; - - review(); + }); }; diff --git a/lib/response.js b/lib/response.js new file mode 100755 index 000000000..654773fcf --- /dev/null +++ b/lib/response.js @@ -0,0 +1,323 @@ +// Load modules + +var Stream = require('stream'); +var NodeUtil = require('util'); +var Shot = require('shot'); +var Utils = require('./utils'); +var Err = require('./error'); + + +// Declare internals + +var internals = {}; + + +// Base response + +exports.Base = internals.Base = function () { + + this.code = 200; + this.headers = {}; + this.payload = null; + this.options = {}; + this._tag = 'generic'; + + return this; +}; + + +internals.Base.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.Base.prototype.header = function (key, value) { + + this.headers[key] = value; + return this; +}; + + +internals.Base.prototype.type = function (type) { + + this.headers['Content-Type'] = type; + return this; +}; + + +internals.Base.prototype.bytes = function (bytes) { + + this.headers['Content-Length'] = bytes; + return this; +}; + + +internals.Base.prototype.created = function (uri) { + + this.code = 201; + this.headers['Location'] = uri; + return this; +}; + + +internals.Base.prototype.ttl = function (ttlMsec, isOverride) { // isOverride defaults to true + + this.ttlMsec = (isOverride === false ? (this.ttlMsec ? this.ttlMsec : ttlMsec) : ttlMsec); + return this; +}; + + +// Empty response + +exports.Empty = internals.Empty = function () { + + internals.Base.call(this); + this._tag = 'empty'; + + this.payload = ''; + this.headers['Content-Length'] = 0; + + return this; +}; + +NodeUtil.inherits(internals.Empty, internals.Base); + + +// Obj response + +exports.Obj = internals.Obj = function (object, type) { + + internals.Base.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.headers['Content-Type'] = type || 'application/json'; + this.headers['Content-Length'] = Buffer.byteLength(this.payload); + + return this; +}; + +NodeUtil.inherits(internals.Obj, internals.Base); + + +// Text response + +exports.Text = internals.Text = function (text, type) { + + internals.Base.call(this); + this._tag = 'text'; + + this.payload = text; + this.headers['Content-Type'] = type || 'text/html'; + this.headers['Content-Length'] = Buffer.byteLength(this.payload); + + return this; +}; + +NodeUtil.inherits(internals.Text, internals.Base); + + +// Stream response + +exports.Stream = internals.Stream = function (stream) { + + internals.Base.call(this); + this._tag = 'stream'; + delete this.payload; + + this.stream = stream; + + return this; +}; + +NodeUtil.inherits(internals.Stream, internals.Base); + + +internals.Stream.prototype._transmit = function (request, callback) { + + var self = this; + + // Check if stream is a node HTTP response (stream.*) or a (mikeal's) Request object (stream.response.*) + + if (!request._route || + !request._route.config.proxy || + request._route.config.proxy.passThrough) { // Pass headers only if not proxy or proxy with pass-through set + + var responseHeaders = this.stream.response ? this.stream.response.headers : this.stream.headers; + if (responseHeaders) { + Utils.merge(this.headers, responseHeaders); + } + } + + 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); + + var isEnded = false; + var end = function () { + + if (isEnded) { + return; + } + + isEnded = true; + request.raw.res.end(); + callback(); + }; + + request.raw.req.on('close', function () { + + self.stream.destroy.bind(self.stream); + end(); + }); + + this.stream.on('error', function () { + + request.raw.req.destroy(); + end(); + }); + + this.stream.on('end', function () { + + end(); + }); + + this.stream.resume(); + this.stream.pipe(request.raw.res); +}; + + +// Error response + +internals.Error = function (options) { + + internals.Obj.call(this, options.payload); + this._tag = 'error'; + this.code = options.code; + if (options.type) { + this.headers['Content-Type'] = options.type; + } + + return this; +}; + +NodeUtil.inherits(internals.Error, internals.Obj); + + +// Utilities + +exports.generateResponse = function (result, onSend) { + + var response = null; + + if (result === null || + result === undefined || + result === '') { + + response = new internals.Empty(); + } + else if (typeof result === 'string') { + response = new internals.Text(result); + } + else if (typeof result === 'object') { + if (result instanceof Err) { + response = result; + } + else if (result instanceof Error) { + response = new Err(result); + } + else if (result instanceof Stream) { + response = new internals.Stream(result); + } + else if (result instanceof internals.Base) { + response = result; + } + } + + if (!response) { + response = new internals.Obj(result); + } + + Utils.assert(response && (response instanceof internals.Base || response instanceof Error), 'Response must be an instance of Error or Base'); // Safety + + if (onSend) { + response.send = function () { + + delete response.send; + onSend(); + }; + } + + return response; +}; + + +exports._respond = function (response, request, callback) { + + if (!response || + (!(response instanceof internals.Base) && !(response instanceof Err))) { + + response = Err.internal('Unexpected response object', response); + } + + // 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()); + + request.log(['http', 'response', 'error'], response); + response = new internals.Error(errOptions); + } + + // Normalize Location header + + if (response.headers.Location) { + var uri = response.headers.Location; + var isAbsolute = (uri.indexOf('http://') === 0 || uri.indexOf('https://') === 0); + response.headers.Location = (isAbsolute ? uri : request.server.settings.uri + (uri.charAt(0) === '/' ? '' : '/') + uri); + } + + // Caching headers + + response.header('Cache-Control',response.ttlMsec ? 'max-age=' + Math.floor(response.ttlMsec / 1000) : 'must-revalidate'); + + // 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); + } + + // Injection + + if (response.payload !== undefined) { // Value can be falsey + if (Shot.isInjection(request.raw.req)) { + request.raw.res.hapi = { result: response.raw || response.payload }; + } + } + + // Response object + + response._transmit(request, function () { + + request.log(['http', 'response', response._tag]); + return callback(); + }); +}; + + + + + diff --git a/lib/route.js b/lib/route.js index 768351911..d462a0463 100755 --- a/lib/route.js +++ b/lib/route.js @@ -23,9 +23,6 @@ exports = module.exports = internals.Route = function (options, server) { Utils.assert(settings.path.match(internals.Route.pathRegex), 'Invalid path: ' + settings.path); Utils.assert(settings.method, 'Route options missing method'); - settings.validate = settings.validate || {}; - Utils.assert(!settings.validate.schema || !settings.payload || settings.payload === 'parse', 'Route payload must be set to \'parse\' when schema validation enabled'); - this.server = server; this.method = settings.method.toLowerCase(); this.path = settings.path; @@ -39,6 +36,8 @@ exports = module.exports = internals.Route = function (options, server) { // Default is 'parse' for POST and PUT otherwise 'stream' this.config.validate = this.config.validate || {}; + Utils.assert(!this.config.validate.schema || !this.config.payload || this.config.payload === 'parse', 'Route payload must be set to \'parse\' when schema validation enabled'); + this.config.payload = this.config.payload || (this.config.validate.schema || this.method === 'post' || this.method === 'put' ? 'parse' : 'stream'); Utils.assert(['stream', 'raw', 'parse'].indexOf(this.config.payload) !== -1, 'Unknown route payload mode: ' + this.config.payload); diff --git a/test/integration/cache.js b/test/integration/cache.js index 53e6800da..765d03b0d 100755 --- a/test/integration/cache.js +++ b/test/integration/cache.js @@ -27,7 +27,7 @@ describe('Cache', function() { var badHandler = function (request) { - request.reply.stream(new Stream); + request.reply(new Stream); }; var errorHandler = function (request) { diff --git a/test/integration/docs.js b/test/integration/docs.js index 2a4a3d737..a1301d628 100755 --- a/test/integration/docs.js +++ b/test/integration/docs.js @@ -4,7 +4,7 @@ var expect = require('chai').expect; var Hapi = process.env.TEST_COV ? require('../../lib-cov/hapi') : require('../../lib/hapi'); var S = Hapi.Types.String; -describe('Docs Generator', function() { +describe('Docs Generator', function () { var _routeTemplate = '{{#each routes}}{{this.method}}|{{/each}}'; var _indexTemplate = '{{#each routes}}{{this.path}}|{{/each}}'; @@ -13,18 +13,18 @@ describe('Docs Generator', function() { var _serverUrl = 'http://127.0.0.1:8083'; var _serverWithoutPostUrl = 'http://127.0.0.1:18083'; - var handler = function(request) { + var handler = function (request) { request.reply('ok'); }; function setupServer(done) { - _server = new Hapi.Server('0.0.0.0', 8083, { docs: { routeTemplate: _routeTemplate, indexTemplate: _indexTemplate }}); + _server = new Hapi.Server('0.0.0.0', 8083, { docs: { routeTemplate: _routeTemplate, indexTemplate: _indexTemplate } }); _server.addRoutes([ { method: 'GET', path: '/test', config: { handler: handler, query: { param1: S().required() } } }, { method: 'POST', path: '/test', config: { handler: handler, query: { param2: S().valid('first', 'last') } } } ]); - _server.listener.on('listening', function() { + _server.listener.on('listening', function () { done(); }); _server.start(); @@ -32,11 +32,11 @@ describe('Docs Generator', function() { function setupServerWithoutPost(done) { - _serverWithoutPost = new Hapi.Server('0.0.0.0', 18083, { docs: { routeTemplate: _routeTemplate, indexTemplate: _indexTemplate }}); + _serverWithoutPost = new Hapi.Server('0.0.0.0', 18083, { docs: { routeTemplate: _routeTemplate, indexTemplate: _indexTemplate } }); _serverWithoutPost.addRoutes([ { method: 'GET', path: '/test', config: { handler: handler, query: { param1: S().required() } } } ]); - _serverWithoutPost.listener.on('listening', function() { + _serverWithoutPost.listener.on('listening', function () { done(); }); _serverWithoutPost.start(); @@ -44,7 +44,7 @@ describe('Docs Generator', function() { function makeRequest(path, callback) { - var next = function(res) { + var next = function (res) { return callback(res.result); }; @@ -56,48 +56,48 @@ describe('Docs Generator', function() { before(setupServer); - it('shows template when correct path is provided', function(done) { + it('shows template when correct path is provided', function (done) { - makeRequest('/docs?path=/test', function(res) { + makeRequest('/docs?path=/test', function (res) { expect(res).to.equal('GET|POST|'); done(); }); }); - it('has a Not Found response when wrong path is provided', function(done) { + it('has a Not Found response when wrong path is provided', function (done) { - makeRequest('/docs?path=blah', function(res) { + makeRequest('/docs?path=blah', function (res) { expect(res.error).to.equal('Not Found'); done(); }); }); - it('displays the index if no path is provided', function(done) { + it('displays the index if no path is provided', function (done) { - makeRequest('/docs', function(res) { + makeRequest('/docs', function (res) { expect(res).to.equal('/test|/test|'); done(); }); }); - it('the index does\'t have the docs endpoint listed', function(done) { + it('the index does\'t have the docs endpoint listed', function (done) { - makeRequest('/docs', function(res) { + makeRequest('/docs', function (res) { expect(res).to.not.contain('/docs'); done(); }); }); - describe('Index', function() { + describe('Index', function () { before(setupServerWithoutPost); - it('doesn\'t throw an error when requesting the index when there are no POST routes', function(done) { + it('doesn\'t throw an error when requesting the index when there are no POST routes', function (done) { _serverWithoutPost.inject({ method: 'get', url: _serverWithoutPostUrl + '/docs' - }, function(res) { + }, function (res) { expect(res).to.exist; expect(res.result).to.contain('/test'); diff --git a/test/integration/gzip.js b/test/integration/gzip.js old mode 100644 new mode 100755 index 1eead9923..e30bf1415 --- a/test/integration/gzip.js +++ b/test/integration/gzip.js @@ -3,87 +3,99 @@ var expect = require('chai').expect; var libPath = process.env.TEST_COV ? '../../lib-cov/' : '../../lib/'; var Hapi = require(libPath + 'hapi'); -var Zlib = require("zlib"); +var Zlib = require('zlib'); -describe('Payload', function() { +describe('Payload', function () { + var server = new Hapi.Server('0.0.0.0', 8080); - var message = {"msg": "This message is going to be gzipped."}; + var message = { 'msg': 'This message is going to be gzipped.' }; var badMessage = '{ this is just wrong }'; - + var postHandler = { - method: 'POST', + method: 'POST', path: '/', config: { - handler: function(req) { + handler: function (req) { + req.reply(req.payload) } } - } + }; + server.addRoute(postHandler); - - it('returns without error if given gzipped payload', function(done) { + + it('returns without error if given gzipped payload', function (done) { + var input = JSON.stringify(message); - - Zlib.deflate(input, function(err, buf) { + + Zlib.deflate(input, function (err, buf) { + var request = { method: 'POST', url: '/', headers: { - 'content-type': "application/json", - 'content-encoding': "gzip", - 'content-length': buf.length + 'content-type': 'application/json', + 'content-encoding': 'gzip', + 'content-length': buf.length }, payload: buf - } - + }; + server.inject(request, function (res) { + expect(res.result).to.exist; expect(res.result).to.deep.equal(message); done(); - }) - }) - }) - - it('returns without error if given non-gzipped payload', function(done) { + }); + }); + }); + + it('returns without error if given non-gzipped payload', function (done) { + var payload = JSON.stringify(message); - + var request = { method: 'POST', url: '/', headers: { - 'content-type': "application/json", - 'content-length': payload.length + 'content-type': 'application/json', + 'content-length': payload.length }, payload: payload - } - + }; + server.inject(request, function (res) { + expect(res.result).to.exist; expect(res.result).to.deep.equal(message); done(); - }) - }) - - it('returns error if given non-JSON gzipped payload when expecting gzip', function(done) { - Zlib.deflate(badMessage, function(err, buf) { + });; + }); + + it('returns error if given non-JSON gzipped payload when expecting gzip', function (done) { + + Zlib.deflate(badMessage, function (err, buf) { + var request = { method: 'POST', url: '/', headers: { - 'content-type': "application/json", - 'content-encoding': "gzip", - 'content-length': buf.length + 'content-type': 'application/json', + 'content-encoding': 'gzip', + 'content-length': buf.length }, payload: buf - } - + }; + server.inject(request, function (res) { + expect(res.result).to.exist; expect(res.result.message).to.exist; expect(res.result.message).to.equal('Invalid JSON body'); done(); - }) - }) - }) -}) \ No newline at end of file + }); + }); + }); +}); + diff --git a/test/integration/proxy.js b/test/integration/proxy.js index ad42671cb..bf72f39c6 100755 --- a/test/integration/proxy.js +++ b/test/integration/proxy.js @@ -96,10 +96,10 @@ describe('Proxy', function () { function item(request) { - request.reply.created('http://google.com')({ + request.reply.payload({ 'id': '55cf687663', 'name': 'Item' - }); + }).created('http://google.com').send(); } function echoPostBody(request) { @@ -119,8 +119,7 @@ describe('Proxy', function () { function postResponse(request, settings, response, payload) { - request.reply.type(response.headers['content-type']); - request.reply(payload); + request.reply.payload(payload).type(response.headers['content-type']).send(); } function makeRequest(options, callback) { diff --git a/test/integration/response.js b/test/integration/response.js new file mode 100755 index 000000000..18ca55ca4 --- /dev/null +++ b/test/integration/response.js @@ -0,0 +1,203 @@ +// Load modules + +var expect = require('chai').expect; +var libPath = process.env.TEST_COV ? '../../lib-cov/' : '../../lib/'; +var Hapi = require(libPath + 'hapi'); +var Zlib = require('zlib'); +var NodeUtil = require('util'); +var Stream = require('stream'); + + +describe('Response', function () { + + var server = new Hapi.Server('0.0.0.0', 8080); + + var textHandler = function (request) { + + request.reply.payload('text').type('text/plain').bytes(4).send(); + }; + + var errorHandler = function (request) { + + request.reply.payload(new Error('boom')).send(); + }; + + var emptyHandler = function (request) { + + request.reply(); + }; + + var baseHandler = function (request) { + + request.reply(new Hapi.Response.Text('hola')); + }; + + var expHandler = function (request) { + + Hapi.Response._respond(null, request, function () { }); + }; + + + FakeStream = function (issue) { + + Stream.call(this); + this.pause = this.resume = this.setEncoding = function () { }; + this.issue = issue; + return this; + }; + + NodeUtil.inherits(FakeStream, Stream); + + + FakeStream.prototype.on = FakeStream.prototype.addListener = function (event, callback) { + + switch (this.issue) { + case 'error': + if (event === 'error') { + if (!this.x) { + callback(); + this.x = true; + } + } + break; + + case 'double': + if (event === 'data') { + callback('x'); + this.x(); + this.y(); + } + else if (event === 'error') { + if (!this.x) { + this.x = callback; + } + } + else if (event === 'end') { + if (!this.y) { + this.y = callback; + } + } + break; + + default: + if (event === 'data') { + callback('x'); + this.x(); + } + else if (event === 'end') { + this.x = callback; + } + break; + } + }; + + var streamHandler = function (request) { + + request.reply.stream(new FakeStream(request.params.issue)).bytes(request.params.issue ? 0 : 1).send(); + }; + + server.addRoutes([ + { 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: '/exp', handler: expHandler }, + { method: 'POST', path: '/stream/{issue?}', handler: streamHandler } + ]); + + it('returns a text reply', function (done) { + + var request = { method: 'POST', url: '/text' }; + + server.inject(request, function (res) { + + expect(res.result).to.exist; + expect(res.result).to.equal('text'); + done(); + }); + }); + + it('returns an error reply', function (done) { + + var request = { method: 'POST', url: '/error' }; + + server.inject(request, function (res) { + + expect(res.statusCode).to.equal(500); + expect(res.result).to.exist; + expect(res.result.message).to.equal('boom'); + done(); + }); + }); + + it('returns an empty reply', function (done) { + + var request = { method: 'POST', url: '/empty' }; + + server.inject(request, function (res) { + + expect(res.result).to.exist; + expect(res.result).to.equal(''); + done(); + }); + }); + + it('returns a base reply', function (done) { + + var request = { method: 'POST', url: '/base' }; + + server.inject(request, function (res) { + + expect(res.result).to.exist; + expect(res.result).to.equal('hola'); + done(); + }); + }); + + it('returns an error reply on invalid Response._respond', function (done) { + + var request = { method: 'POST', url: '/exp' }; + + server.inject(request, function (res) { + + expect(res.statusCode).to.equal(500); + expect(res.result).to.exist; + expect(res.result.message).to.equal('An internal server error occurred'); + done(); + }); + }); + + it('returns a stream reply', function (done) { + + var request = { method: 'POST', url: '/stream' }; + + server.inject(request, function (res) { + + expect(res.readPayload()).to.equal('x'); + done(); + }); + }); + + it('returns a broken stream reply on error issue', function (done) { + + var request = { method: 'POST', url: '/stream/error' }; + + server.inject(request, function (res) { + + expect(res.readPayload()).to.equal(''); + done(); + }); + }); + + it('returns a broken stream reply on double issue', function (done) { + + var request = { method: 'POST', url: '/stream/double' }; + + server.inject(request, function (res) { + + expect(res.readPayload()).to.equal('x'); + done(); + }); + }); +}); + diff --git a/test/unit/error.js b/test/unit/error.js index 0c0971079..6e8b9b7fe 100755 --- a/test/unit/error.js +++ b/test/unit/error.js @@ -1,73 +1,96 @@ var expect = require('chai').expect; var Err = process.env.TEST_COV ? require('../../lib-cov/error') : require('../../lib/error'); -describe('Err', function() { - describe('#badRequest', function() { +describe('Err', function () { + + describe('#Error', function () { + + it ('returns an error with info when constructed using another error and message', function (done) { + + var err = new Err(new Error('inner'), 'outter'); + expect(err.message).to.equal('inner'); + expect(err.info).to.equal('outter'); + done(); + }); + }); + + describe('#badRequest', function () { + + it('returns a 400 error code', function (done) { - it('returns a 400 error code', function(done) { expect(Err.badRequest().code).to.equal(400); done(); }); - it('sets the message with the passed in message', function(done) { + it('sets the message with the passed in message', function (done) { + expect(Err.badRequest('my message').message).to.equal('my message'); done(); }); }); - describe('#unauthorized', function() { + describe('#unauthorized', function () { + + it('returns a 401 error code', function (done) { - it('returns a 401 error code', function(done) { expect(Err.unauthorized().code).to.equal(401); done(); }); - it('sets the message with the passed in message', function(done) { + it('sets the message with the passed in message', function (done) { + expect(Err.unauthorized('my message').message).to.equal('my message'); done(); }); }); - describe('#forbidden', function() { + describe('#forbidden', function () { + + it('returns a 403 error code', function (done) { - it('returns a 403 error code', function(done) { expect(Err.forbidden().code).to.equal(403); done(); }); - it('sets the message with the passed in message', function(done) { + it('sets the message with the passed in message', function (done) { + expect(Err.forbidden('my message').message).to.equal('my message'); done(); }); }); - describe('#notFound', function() { + describe('#notFound', function () { + + it('returns a 404 error code', function (done) { - it('returns a 404 error code', function(done) { expect(Err.notFound().code).to.equal(404); done(); }); - it('sets the message with the passed in message', function(done) { + it('sets the message with the passed in message', function (done) { + expect(Err.notFound('my message').message).to.equal('my message'); done(); }); }); - describe('#internal', function() { + describe('#internal', function () { + + it('returns a 500 error code', function (done) { - it('returns a 500 error code', function(done) { expect(Err.internal().code).to.equal(500); done(); }); - it('sets the message with the passed in message', function(done) { + it('sets the message with the passed in message', function (done) { + expect(Err.internal('my message').message).to.equal('my message'); done(); }); - it('passes data on the callback if its passed in', function(done) { + it('passes data on the callback if its passed in', function (done) { + expect(Err.internal('my message', { my: 'data' }).data.my).to.equal('data'); done(); }); @@ -77,51 +100,15 @@ describe('Err', function() { it('formats a custom error', function (done) { - var err = new Err(500, 'Unknown', { - toResponse: function () { + var err = new Err(500, 'Unknown'); + err.toResponse = function () { - return { payload: { test: true } }; - } - }); + return { payload: { test: true } }; + }; expect(err.toResponse().payload.test).to.equal(true); done(); }); - - it('formats a generic error', function (done) { - - var err = new Error('boom'); - err.x = 10; - - var response = Err.toResponse(err); - expect(response.payload.message).to.equal('boom'); - expect(response.payload.x).to.equal(10); - expect(response.code).to.equal(500); - done(); - }); - - it('formats an hapi error', function (done) { - - var err = new Err(500, 'boom'); - err.x = 10; - - var response = Err.toResponse(err); - expect(response.payload.message).to.equal('boom'); - expect(response.payload.x).to.equal(10); - expect(response.code).to.equal(500); - done(); - }); - - it('formats an internal error', function (done) { - - var err = Err.internal('boom', { x: 10 }); - - var response = Err.toResponse(err); - expect(response.payload.message).to.equal('An internal server error occurred'); - expect(response.payload.x).to.not.exist; - expect(response.code).to.equal(500); - done(); - }); }); }); diff --git a/test/unit/server.js b/test/unit/server.js index 5d4a5598b..e4eaf318d 100755 --- a/test/unit/server.js +++ b/test/unit/server.js @@ -110,6 +110,14 @@ describe('Server', function () { done(); }); + it('doesn\'t throw an error when enabling auth', function (done) { + var fn = function () { + var server = new Server('0.0.0.0', 8086, { auth: { scheme: 'basic' } }); + }; + expect(fn).to.not.throw(Error); + done(); + }); + describe('#_match', function () {