hapi-dev-errors
Advanced tools
Comparing version 3.2.0 to 3.2.1
@@ -8,4 +8,4 @@ { | ||
"parserOptions": { | ||
"ecmaVersion": 8 | ||
"ecmaVersion": 2018 | ||
} | ||
} |
# Changelog | ||
## Version [3.2.1](https://github.com/fs-opensource/hapi-dev-errors/compare/v3.2.0...v3.2.1) (2019-01-22) | ||
### Updated | ||
- use `request.path` over `request.url.path` to support hapi 18 | ||
- refactor plugin: error handler class and plugin entry point | ||
- split plugin handling and extension point registration from request handling | ||
- move icons (Google, Stack Overflow) to SVG files | ||
- refine texts in Readme | ||
- refactor code in examples | ||
- bump dependencies | ||
- update tests to use `it` over `test` | ||
## Version [3.2.0](https://github.com/fs-opensource/hapi-dev-errors/compare/v3.1.0...v3.2.0) (2018-10-10) | ||
- `add` new option [`links`](https://github.com/fs-opensource/hapi-dev-errors#plugin-registration-options) which represents an array of callback functions to render helpful links. By default, `hapi-dev-errors` renders linked SVG icons for Google and Stack Overflow to search for help based on the error message | ||
## Version [3.1.0](https://github.com/fs-opensource/hapi-dev-errors/compare/v3.0.1...v3.1.0) (2018-09-28) | ||
@@ -7,0 +21,0 @@ - `add` pass `request` to custom view: this allows you to access every request property |
@@ -5,8 +5,3 @@ 'use strict' | ||
// create new server instance | ||
// add serverβs connection information | ||
const server = new Hapi.Server({ | ||
host: 'localhost', | ||
port: 3000 | ||
}) | ||
const server = new Hapi.Server({ host: 'localhost', port: 3000 }) | ||
@@ -22,5 +17,5 @@ async function launchIt () { | ||
server.route({ | ||
method: 'GET', | ||
method: '*', | ||
path: '/{path*}', | ||
handler: (request, h) => { | ||
handler: (_, h) => { | ||
return h.notAvailable() | ||
@@ -30,10 +25,6 @@ } | ||
try { | ||
await server.start() | ||
console.log('Server running at: ' + server.info.uri) | ||
} catch (err) { | ||
throw err | ||
} | ||
await server.start() | ||
console.log('Server running at: ' + server.info.uri) | ||
} | ||
launchIt() |
@@ -6,8 +6,3 @@ 'use strict' | ||
// create new server instance | ||
// add serverβs connection information | ||
const server = new Hapi.Server({ | ||
host: 'localhost', | ||
port: 3000 | ||
}) | ||
const server = new Hapi.Server({ host: 'localhost', port: 3000 }) | ||
@@ -38,5 +33,5 @@ async function launchIt () { | ||
server.route({ | ||
method: 'GET', | ||
method: '*', | ||
path: '/{path*}', | ||
handler: (request, reply) => { | ||
handler: (_, reply) => { | ||
reply.notAvailable() | ||
@@ -46,10 +41,6 @@ } | ||
try { | ||
await server.start() | ||
console.log('Server running at: ' + server.info.uri) | ||
} catch (err) { | ||
throw err | ||
} | ||
await server.start() | ||
console.log('Server running at: ' + server.info.uri) | ||
} | ||
launchIt() |
@@ -5,8 +5,3 @@ 'use strict' | ||
// create new server instance | ||
// add serverβs connection information | ||
const server = new Hapi.Server({ | ||
host: 'localhost', | ||
port: 3000 | ||
}) | ||
const server = new Hapi.Server({ host: 'localhost', port: 3000 }) | ||
@@ -22,3 +17,3 @@ async function launchIt () { | ||
return `<a rel="noopener noreferrer" target="_blank" href="https://github.com/fs-opensource/hapi-dev-errors/search?q=${error.message}"> | ||
Search Youch on GitHub | ||
Search hapi-dev-errors on GitHub | ||
</a>` | ||
@@ -33,3 +28,3 @@ } | ||
path: '/{path*}', | ||
handler: (request, h) => { | ||
handler: (_, h) => { | ||
h.notAvailable() | ||
@@ -39,10 +34,6 @@ } | ||
try { | ||
await server.start() | ||
console.log('Server running at: ' + server.info.uri) | ||
} catch (err) { | ||
throw err | ||
} | ||
await server.start() | ||
console.log('Server running at: ' + server.info.uri) | ||
} | ||
launchIt() |
200
lib/index.js
'use strict' | ||
const Youch = require('youch') | ||
const ForTerminal = require('youch-terminal') | ||
const ErrorHandler = require('./error-handler') | ||
/** | ||
* Create a Youch instance for pretty error printing. | ||
* This instance is used to format output for the | ||
* console and for a web view. | ||
* | ||
* @param {Object} request - the request object | ||
* @param {Object} error - object with error details | ||
* | ||
* @returns {Object} | ||
*/ | ||
function createYouch ({ request, error, links = [] }) { | ||
/** | ||
* hapiβs request and error objects donβt match the | ||
* expected structure in Youch. We need to adjust | ||
* properties to display them correctly. | ||
*/ | ||
request.url = request.path | ||
request.httpVersion = request.raw.req.httpVersion | ||
error.status = error.output.statusCode | ||
try { | ||
const youch = new Youch(error, request) | ||
links.forEach(link => youch.addLink(link)) | ||
return youch | ||
} catch (error) { | ||
console.error(error) | ||
throw error | ||
} | ||
} | ||
/** | ||
* Check whether the incoming request requires a JSON response. | ||
* This is true for requests where the "accept" header | ||
* contains "json" or the agent is a CLI/GUI app. | ||
* | ||
* @param {Object} | ||
* | ||
* @returns {Boolean} | ||
*/ | ||
function wantsJson ({ agent, accept }) { | ||
return matches(agent, /curl|wget|postman|insomnia/i) || matches(accept, /json/) | ||
} | ||
/** | ||
* Helper function to test whether a given | ||
* string matches a RegEx. | ||
* | ||
* @param {String} str | ||
* @param {String} regex | ||
* | ||
* @returns {Boolean} | ||
*/ | ||
function matches (str, regex) { | ||
return str && str.match(regex) | ||
} | ||
/** | ||
* Returns a link to Google that includes | ||
* the error message as the search | ||
* term. The link is an SVG icon. | ||
* | ||
* @param {Object} error | ||
* | ||
* @returns {String} | ||
*/ | ||
function googleIcon (error) { | ||
return `<a rel="noopener noreferrer" target="_blank" href="https://google.com/search?q=${encodeURIComponent(error.message)}" title="Search Google for "${error.message}""> | ||
<!-- Google icon by Picons.me, found at https://www.iconfinder.com/Picons --> | ||
<!-- Free for commercial use --> | ||
<svg width="24" height="24" viewBox="0 0 56.6934 56.6934" xmlns="http://www.w3.org/2000/svg"> | ||
<path d="M51.981,24.4812c-7.7173-0.0038-15.4346-0.0019-23.1518-0.001c0.001,3.2009-0.0038,6.4018,0.0019,9.6017 c4.4693-0.001,8.9386-0.0019,13.407,0c-0.5179,3.0673-2.3408,5.8723-4.9258,7.5991c-1.625,1.0926-3.492,1.8018-5.4168,2.139 c-1.9372,0.3306-3.9389,0.3729-5.8713-0.0183c-1.9651-0.3921-3.8409-1.2108-5.4773-2.3649 c-2.6166-1.8383-4.6135-4.5279-5.6388-7.5549c-1.0484-3.0788-1.0561-6.5046,0.0048-9.5805 c0.7361-2.1679,1.9613-4.1705,3.5708-5.8002c1.9853-2.0324,4.5664-3.4853,7.3473-4.0811c2.3812-0.5083,4.8921-0.4113,7.2234,0.294 c1.9815,0.6016,3.8082,1.6874,5.3044,3.1163c1.5125-1.5039,3.0173-3.0164,4.527-4.5231c0.7918-0.811,1.624-1.5865,2.3908-2.4196 c-2.2928-2.1218-4.9805-3.8274-7.9172-4.9056C32.0723,4.0363,26.1097,3.995,20.7871,5.8372 C14.7889,7.8907,9.6815,12.3763,6.8497,18.0459c-0.9859,1.9536-1.7057,4.0388-2.1381,6.1836 C3.6238,29.5732,4.382,35.2707,6.8468,40.1378c1.6019,3.1768,3.8985,6.001,6.6843,8.215c2.6282,2.0958,5.6916,3.6439,8.9396,4.5078 c4.0984,1.0993,8.461,1.0743,12.5864,0.1355c3.7284-0.8581,7.256-2.6397,10.0725-5.24c2.977-2.7358,5.1006-6.3403,6.2249-10.2138 C52.5807,33.3171,52.7498,28.8064,51.981,24.4812z"/> | ||
</svg> | ||
</a>` | ||
} | ||
/** | ||
* Returns a link to Stack Overflow that | ||
* includes the error message as the | ||
* search term. The link is an SVG icon. | ||
* | ||
* @param {Object} error | ||
* | ||
* @returns {String} | ||
*/ | ||
function stackOverflowIcon (error) { | ||
return `<a rel="noopener noreferrer" target="_blank" href="https://stackoverflow.com/search?q=${encodeURIComponent(error.message)}" title="Search Stack Overflow for "${error.message}""> | ||
<!-- Stack Overflow icon by Picons.me, found at https://www.iconfinder.com/Picons --> | ||
<!-- Free for commercial use --> | ||
<svg width="24" height="24" viewBox="-1163 1657.697 56.693 56.693" xmlns="http://www.w3.org/2000/svg"> | ||
<rect height="4.1104" transform="matrix(-0.8613 -0.508 0.508 -0.8613 -2964.1831 2556.6357)" width="19.2465" x="-1142.8167" y="1680.7778"/><rect height="4.1105" transform="matrix(-0.9657 -0.2596 0.2596 -0.9657 -2672.0498 3027.386)" width="19.2462" x="-1145.7363" y="1688.085"/><rect height="4.1098" transform="matrix(-0.9958 -0.0918 0.0918 -0.9958 -2425.5647 3282.8535)" width="19.246" x="-1146.9451" y="1695.1263"/><rect height="4.111" width="19.2473" x="-1147.2625" y="1701.293"/><path d="M-1121.4579,1710.9474c0,0,0,0.9601-0.0323,0.9601v0.0156h-30.7953c0,0-0.9598,0-0.9598-0.0156h-0.0326v-20.03h3.2877 v16.8049h25.2446v-16.8049h3.2877V1710.9474z"/><rect height="4.111" transform="matrix(0.5634 0.8262 -0.8262 0.5634 892.9033 1662.7915)" width="19.247" x="-1136.5389" y="1674.2235"/><rect height="4.1108" transform="matrix(0.171 0.9853 -0.9853 0.171 720.9987 2489.031)" width="19.2461" x="-1128.3032" y="1670.9347"/> | ||
</svg> | ||
</a>` | ||
} | ||
/** | ||
* Render better error views during development. | ||
* | ||
* @param {Object} server - hapi server instance where the plugin is registered | ||
* @param {Object} options - plugin options | ||
*/ | ||
async function register (server, options) { | ||
const defaults = { | ||
showErrors: false, | ||
toTerminal: true, | ||
links: [ | ||
(error) => googleIcon(error), | ||
(error) => stackOverflowIcon(error) | ||
] | ||
} | ||
const { showErrors = false, template, ...config } = options | ||
const config = Object.assign({}, defaults, options) | ||
/** | ||
* Cut early if `showErrors` is false. No need to | ||
* hook the extension point in production. | ||
* Cut early and donβt register extension point, | ||
* e.g. when in production. | ||
*/ | ||
if (!config.showErrors) { | ||
if (!showErrors) { | ||
return | ||
} | ||
// require `vision` plugin in case the user provides an error template | ||
if (config.template) { | ||
/** | ||
* Ensure the user server is able to render | ||
* templates when going for a custom one. | ||
*/ | ||
if (template) { | ||
server.dependency(['vision']) | ||
} | ||
// Make sure the `links` are an array | ||
if (!Array.isArray(config.links)) { | ||
config.links = [config.links] | ||
} | ||
/** | ||
* Alright, go for gold! | ||
*/ | ||
const errorHandler = new ErrorHandler({ template, ...config }) | ||
// extend the request lifecycle at `onPreResponse` | ||
// to change the default error handling behavior (if enabled) | ||
server.ext('onPreResponse', async (request, h) => { | ||
const error = request.response | ||
// only show `bad implementation` developer errors (status code 500) | ||
if (error.isBoom && error.output.statusCode === 500) { | ||
const accept = request.raw.req.headers.accept | ||
const agent = request.raw.req.headers['user-agent'] | ||
const statusCode = error.output.statusCode | ||
const errorResponse = { | ||
title: error.output.payload.error, | ||
statusCode, | ||
message: error.message, | ||
method: request.raw.req.method, | ||
url: request.url.path, | ||
headers: request.raw.req.headers, | ||
payload: request.raw.req.method !== 'GET' ? request.payload : '', | ||
stacktrace: error.stack | ||
} | ||
const youch = createYouch({ request, error, links: config.links }) | ||
// print a pretty error to terminal as well | ||
if (config.toTerminal) { | ||
const json = await youch.toJSON() | ||
console.log(ForTerminal(json)) | ||
} | ||
// take priority: | ||
// - check "agent" header for REST request (cURL, Postman & Co.) | ||
// - check "accept" header for JSON request | ||
if (wantsJson({ accept, agent })) { | ||
const details = Object.assign({}, errorResponse, { | ||
stacktrace: errorResponse.stacktrace.split('\n').map(line => line.trim()) | ||
}) | ||
return h | ||
.response(JSON.stringify(details, null, 2)) | ||
.type('application/json') | ||
.code(statusCode) | ||
} | ||
// did the user explicitly specify an error template | ||
// favor a userβs custom template over the default template | ||
if (config.template) { | ||
return h | ||
.view(config.template, { request, error, ...errorResponse }) | ||
.code(statusCode) | ||
} | ||
// render Youch HTML template | ||
const html = await youch.toHTML() | ||
return h | ||
.response(html) | ||
.type('text/html') | ||
.code(statusCode) | ||
} | ||
// no developer error, go ahead with the response | ||
return h.continue | ||
return errorHandler.handle(request, h) | ||
}) | ||
@@ -202,0 +32,0 @@ } |
{ | ||
"name": "hapi-dev-errors", | ||
"description": "Return better error details and skip the look at command line to catch the issue.", | ||
"version": "3.2.0", | ||
"version": "3.2.1", | ||
"author": "Future Studio <info@futurestud.io>", | ||
@@ -14,19 +14,19 @@ "bugs": { | ||
"devDependencies": { | ||
"boom": "~7.2.0", | ||
"code": "~5.2.0", | ||
"eslint": "~4.19.1", | ||
"eslint-config-standard": "~11.0.0", | ||
"eslint-plugin-import": "~2.13.0", | ||
"eslint-plugin-node": "~6.0.1", | ||
"eslint-plugin-promise": "~3.8.0", | ||
"eslint-plugin-standard": "~3.1.0", | ||
"hapi": "~17.6.0", | ||
"husky": "~1.1.1", | ||
"joi": "~13.7.0", | ||
"lab": "~15.5.0", | ||
"sinon": "~6.3.5", | ||
"vision": "~5.4.0" | ||
"boom": "~7.3.0", | ||
"code": "~5.2.4", | ||
"eslint": "~5.12.1", | ||
"eslint-config-standard": "~12.0.0", | ||
"eslint-plugin-import": "~2.14.0", | ||
"eslint-plugin-node": "~8.0.1", | ||
"eslint-plugin-promise": "~4.0.1", | ||
"eslint-plugin-standard": "~4.0.0", | ||
"hapi": "~18.0.0", | ||
"husky": "~1.3.1", | ||
"joi": "~14.3.1", | ||
"lab": "~18.0.1", | ||
"sinon": "~7.2.2", | ||
"vision": "~5.4.4" | ||
}, | ||
"engines": { | ||
"node": ">=8.0.0" | ||
"node": ">=8" | ||
}, | ||
@@ -33,0 +33,0 @@ "homepage": "https://github.com/fs-opensource/hapi-dev-errors#readme", |
@@ -20,2 +20,3 @@ <div align="center"> | ||
<a href="https://www.npmjs.com/package/hapi-dev-errors"><img src="https://img.shields.io/npm/v/hapi-dev-errors.svg" alt="hapi-dev-errors Version" data-canonical-src="https://img.shields.io/npm/v/hapi-dev-errors.svg" style="max-width:100%;"></a> | ||
<a href="https://greenkeeper.io/" rel="nofollow"><img src="https://camo.githubusercontent.com/dfb11cc7fc0b1600f0ba93236eff58bb592b8500/68747470733a2f2f6261646765732e677265656e6b65657065722e696f2f66732d6f70656e736f757263652f686170692d6465762d6572726f72732e737667" alt="Greenkeeper badge" data-canonical-src="https://badges.greenkeeper.io/fs-opensource/hapi-dev-errors.svg" style="max-width:100%;"></a> | ||
</p> | ||
@@ -117,5 +118,6 @@ <p> | ||
links: [ (error) => { | ||
return `<a href="https://github.com/fs-opensource/hapi-dev-errors/search?q=${error.message}"> | ||
Search Youch on GitHub | ||
</a>` | ||
return ` | ||
<a href="https://github.com/fs-opensource/hapi-dev-errors/search?q=${error.message}"> | ||
Search hapi-dev-errors on GitHub | ||
</a>` | ||
} | ||
@@ -122,0 +124,0 @@ ] |
@@ -9,3 +9,3 @@ 'use strict' | ||
const { experiment, test, before } = (exports.lab = Lab.script()) | ||
const { experiment, it, before } = (exports.lab = Lab.script()) | ||
@@ -36,3 +36,3 @@ experiment('hapi-dev-error falls back to json', () => { | ||
test('test if the plugin responds json with json accept header', async () => { | ||
it('responds json with json accept header', async () => { | ||
const response = await server.inject({ | ||
@@ -51,3 +51,3 @@ url: '/error', | ||
test('test if the plugin responds json with curl user-agent', async () => { | ||
it('responds json with curl user-agent', async () => { | ||
const response = await server.inject({ | ||
@@ -54,0 +54,0 @@ url: '/error', |
@@ -6,2 +6,3 @@ 'use strict' | ||
const Hapi = require('hapi') | ||
const Boom = require('boom') | ||
@@ -24,5 +25,3 @@ const server = new Hapi.Server() | ||
method: 'GET', | ||
handler: () => { | ||
return new Error('failure') | ||
} | ||
handler: () => new Error('failure') | ||
} | ||
@@ -44,2 +43,25 @@ | ||
}) | ||
test('does not render the gorgeous error view for 503 errors', async () => { | ||
const routeOptions = { | ||
path: '/503-unhandled', | ||
method: 'GET', | ||
handler: () => Boom.serverUnavailable('not ready') | ||
} | ||
server.route(routeOptions) | ||
const request = { | ||
url: routeOptions.path, | ||
method: routeOptions.method | ||
} | ||
const response = await server.inject(request) | ||
const payload = JSON.parse(response.payload || '{}') | ||
Code.expect(response.statusCode).to.equal(503) | ||
Code.expect(payload.message).to.equal('not ready') | ||
Code.expect(payload.error).to.equal('Service Unavailable') | ||
}) | ||
}) |
@@ -11,3 +11,3 @@ 'use strict' | ||
const { experiment, test, before, beforeEach } = (exports.lab = Lab.script()) | ||
const { experiment, it, before, beforeEach } = (exports.lab = Lab.script()) | ||
const expect = Code.expect | ||
@@ -30,10 +30,7 @@ | ||
test('test if the plugin is enabled in development for web requests', async () => { | ||
it('is enabled in development for web requests', async () => { | ||
const routeOptions = { | ||
path: '/showErrorsForWeb', | ||
method: 'GET', | ||
handler: () => { | ||
return Boom.badImplementation('a fancy server error') | ||
} | ||
} | ||
handler: () => Boom.badImplementation('a fancy server error') } | ||
@@ -54,3 +51,3 @@ server.route(routeOptions) | ||
test('test if the plugin is enabled in development for JSON/REST requests', async () => { | ||
it('is enabled in development for JSON/REST requests', async () => { | ||
const routeOptions = { | ||
@@ -83,9 +80,7 @@ path: '/showErrorsForREST', | ||
test('test when the error is from a rejected Promise', async () => { | ||
it('test when the error is from a rejected Promise', async () => { | ||
const routeOptions = { | ||
path: '/showPromiseError', | ||
method: 'GET', | ||
handler: () => { | ||
return Promise.reject(new Error('server error')) | ||
} | ||
handler: () => new Error('server error') | ||
} | ||
@@ -112,9 +107,7 @@ | ||
test('test if the request payload is added to the error response', async () => { | ||
it('test if the request payload is added to the error response', async () => { | ||
const routeOptions = { | ||
path: '/with-request-payload', | ||
method: 'POST', | ||
handler: () => { | ||
return Promise.reject(new Error('server error with payload')) | ||
} | ||
handler: () => new Error('server error with payload') | ||
} | ||
@@ -175,3 +168,3 @@ | ||
test('render a template', async () => { | ||
it('render a template', async () => { | ||
const routeOptions = { | ||
@@ -178,0 +171,0 @@ path: '/custom-view', |
@@ -6,6 +6,7 @@ 'use strict' | ||
const Hapi = require('hapi') | ||
const Boom = require('boom') | ||
let server | ||
const { experiment, test, before } = (exports.lab = Lab.script()) | ||
const { experiment, it, before } = (exports.lab = Lab.script()) | ||
const expect = Code.expect | ||
@@ -26,9 +27,7 @@ | ||
test('test if the plugin skips handling for non-error response', async () => { | ||
it('skips handling for non-error response', async () => { | ||
const routeOptions = { | ||
path: '/ok', | ||
method: 'GET', | ||
handler: () => { | ||
return 'ok' | ||
} | ||
handler: () => 'ok' | ||
} | ||
@@ -49,2 +48,20 @@ | ||
}) | ||
it('skips handling for 404 errors', async () => { | ||
const routeOptions = { | ||
path: '/404', | ||
method: 'GET', | ||
handler: () => Boom.notFound() | ||
} | ||
server.route(routeOptions) | ||
const request = { | ||
url: routeOptions.path, | ||
method: routeOptions.method | ||
} | ||
const response = await server.inject(request) | ||
expect(response.statusCode).to.equal(404) | ||
}) | ||
}) |
@@ -8,3 +8,3 @@ 'use strict' | ||
const { experiment, test, beforeEach, afterEach } = (exports.lab = Lab.script()) | ||
const { experiment, it, beforeEach, afterEach } = (exports.lab = Lab.script()) | ||
@@ -43,3 +43,3 @@ experiment('hapi-dev-error handles custom user links', () => { | ||
test('that the plugin works fine with empty links', async () => { | ||
it('works fine with empty links', async () => { | ||
const server = await createServer({ links: [] }) | ||
@@ -58,3 +58,3 @@ | ||
test('that the plugin throws if the links are strings', async () => { | ||
it('throws if the links are strings', async () => { | ||
const server = await createServer({ links: [ 'error' ] }) | ||
@@ -74,3 +74,3 @@ | ||
test('that the plugin throws if the links is not an array of functions', async () => { | ||
it('throws if the links is not an array of functions', async () => { | ||
const server = await createServer({ links: 'error' }) | ||
@@ -90,3 +90,3 @@ | ||
test('that the plugin works fine with a link function', async () => { | ||
it('works fine with a link function', async () => { | ||
const server = await createServer({ links: () => `link` }) | ||
@@ -93,0 +93,0 @@ |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
224944
26
908
180
13