diff --git a/README.md b/README.md index 2658fd6..710b7bf 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,6 @@ Error handler for use in development (debug) and production environments. - When in debug mode, detailed information such as stack traces are returned in the HTTP responses. -JSON is the only supported response format at this time. - -*There are plans to support other formats such as Text, HTML, and XML.* - ## Install ```bash @@ -47,6 +43,15 @@ In LoopBack applications, add the following entry to your } ``` +## Content Type + +Depending on the request header's `Accepts`, response will be returned in + the corresponding content-type, current supported types include: +- JSON (`json`/`application/json`) +- HTML (`html`/`text/html`) + +*There are plans to support other formats such as Text and XML.* + ## Options #### debug diff --git a/lib/content-negotiation.js b/lib/content-negotiation.js new file mode 100644 index 0000000..6222025 --- /dev/null +++ b/lib/content-negotiation.js @@ -0,0 +1,85 @@ +// Copyright IBM Corp. 2016. All Rights Reserved. +// Node module: strong-error-handler +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +var accepts = require('accepts'); +var debug = require('debug')('strong-error-handler:http-response'); +var sendJson = require('./send-json'); +var sendHtml = require('./send-html'); +var util = require('util'); + +module.exports = negotiateContentProducer; + +/** + * Handles req.accepts and req.query._format and options.defaultType + * to resolve the correct operation + * + * @param req request object + * @param {Object} options options of strong-error-handler + * @returns {Function} Opeartion function with signature `fn(res, data)` + */ +function negotiateContentProducer(req, options) { + var SUPPORTED_TYPES = [ + 'application/json', 'json', + 'text/html', 'html', + ]; + + options = options || {}; + var defaultType = 'json'; + + // checking if user provided defaultType is supported + if (options.defaultType) { + if (SUPPORTED_TYPES.indexOf(options.defaultType) > -1) { + debug('Accepting options.defaultType `%s`', options.defaultType); + defaultType = options.defaultType; + } else { + debug('defaultType: `%s` is not supported, ' + + 'falling back to defaultType: `%s`', options.defaultType, defaultType); + } + } + + // decide to use resolvedType or defaultType + // Please note that accepts assumes the order of content-type is provided + // in the priority returned + // example + // Accepts: text/html, */*, application/json ---> will resolve as text/html + // Accepts: application/json, */*, text/html ---> will resolve as application/json + // Accepts: */*, application/json, text/html ---> will resolve as application/json + // eg. Chrome accepts defaults to `text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*` + // In this case `resolvedContentType` will result as: `text/html` due to the order given + var resolvedContentType = accepts(req).types(SUPPORTED_TYPES); + debug('Resolved content-type', resolvedContentType); + var contentType = resolvedContentType || defaultType; + + // to receive _format from user's url param to overide the content type + // req.query (eg /api/Users/1?_format=json will overide content negotiation + // https://github.com/strongloop/strong-remoting/blob/ac3093dcfbb787977ca0229b0f672703859e52e1/lib/http-context.js#L643-L645 + var query = req.query || {}; + if (query._format) { + if (SUPPORTED_TYPES.indexOf(query._format) > -1) { + contentType = query._format; + } else { + // format passed through query but not supported + var msg = util.format('Response _format "%s" is not supported' + + 'used "%s" instead"', query._format, defaultType); + res.header('X-Warning', msg); + debug(msg); + } + } + + debug('Content-negotiation: req.headers.accept: `%s` Resolved as: `%s`', + req.headers.accept, contentType); + return resolveOperation(contentType); +} + +function resolveOperation(contentType) { + switch (contentType) { + case 'application/json': + case 'json': + return sendJson; + case 'text/html': + case 'html': + return sendHtml; + } +} diff --git a/lib/handler.js b/lib/handler.js index bba6ef7..fb6196d 100644 --- a/lib/handler.js +++ b/lib/handler.js @@ -9,7 +9,7 @@ var buildResponseData = require('./data-builder'); var debug = require('debug')('strong-error-handler'); var format = require('util').format; var logToConsole = require('./logger'); -var sendJson = require('./send-json'); +var negotiateContentProducer = require('./content-negotiation'); function noop() { } @@ -52,11 +52,7 @@ exports = module.exports = function createStrongErrorHandler(options) { res.setHeader('X-Content-Type-Options', 'nosniff'); res.statusCode = data.statusCode; - // TODO: negotiate the content-type, take into account options.defaultType - // For now, we always return JSON. See - // - https://github.com/strongloop/strong-error-handler/issues/4 - // - https://github.com/strongloop/strong-error-handler/issues/5 - // - https://github.com/strongloop/strong-error-handler/issues/6 - sendJson(res, data); + var sendResponse = negotiateContentProducer(req, options); + sendResponse(res, data); }; }; diff --git a/lib/send-html.js b/lib/send-html.js new file mode 100644 index 0000000..4b00ca0 --- /dev/null +++ b/lib/send-html.js @@ -0,0 +1,46 @@ +// Copyright IBM Corp. 2016. All Rights Reserved. +// Node module: strong-error-handler +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +var ejs = require('ejs'); +var fs = require('fs'); +var path = require('path'); + +var assetDir = path.resolve(__dirname, '../views'); +var compiledTemplates = { + // loading default template and stylesheet + default: loadDefaultTemplates(), +}; + +module.exports = sendHtml; + +function sendHtml(res, data, options) { + var toRender = {options: {}, data: data}; + // TODO: ability to call non-default template functions from options + var body = compiledTemplates.default(toRender); + sendReponse(res, body); +} + +/** + * Compile and cache the file with the `filename` key in options + * + * @param filepath (description) + * @returns {Function} render function with signature fn(data); + */ +function compileTemplate(filepath) { + var options = {cache: true, filename: filepath}; + fileContent = fs.readFileSync(filepath, 'utf8'); + return ejs.compile(fileContent, options); +} + +// loads and cache default error templates +function loadDefaultTemplates() { + var defaultTemplate = path.resolve(assetDir, 'default-error.ejs'); + return compileTemplate(defaultTemplate); +} + +function sendReponse(res, body) { + res.setHeader('Content-Type', 'text/html; charset=utf-8'); + res.end(body); +} diff --git a/package.json b/package.json index 71cab6c..28921ca 100644 --- a/package.json +++ b/package.json @@ -14,14 +14,20 @@ "posttest": "npm run lint" }, "dependencies": { + "accepts": "^1.3.3", "debug": "^2.2.0", + "ejs": "^2.4.2", "http-status": "^0.2.2" }, "devDependencies": { "chai": "^2.1.1", "eslint": "^2.5.3", "eslint-config-loopback": "^3.0.0", + "express": "^4.13.4", "mocha": "^2.1.0", "supertest": "^1.1.0" + }, + "browser": { + "strong-error-handler": false } } diff --git a/test/handler.test.js b/test/handler.test.js index 4571445..0dad7c9 100644 --- a/test/handler.test.js +++ b/test/handler.test.js @@ -8,7 +8,7 @@ var cloneAllProperties = require('../lib/clone.js'); var debug = require('debug')('test'); var expect = require('chai').expect; -var http = require('http'); +var express = require('express'); var strongErrorHandler = require('..'); var supertest = require('supertest'); var util = require('util'); @@ -361,9 +361,156 @@ describe('strong-error-handler', function() { .expect('Content-Type', /^application\/json/); } }); + + context('HTML response', function() { + it('contains all error properties when debug=true', function(done) { + var error = new ErrorWithProps({ + message: 'a test error message', + details: 'some details', + extra: 'sensitive data', + }); + error.statusCode = 500; + givenErrorHandlerForError(error, {debug: true}); + requestHTML() + .expect(500) + .expect(/
<%- data.stack %>+ <% } + %> +