Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,21 @@ The following plugin options allow you to customize the default behavior of `hap
- **showErrors**: `(boolean)`, default: `false` — by default, the plugin is disabled and keeps hapi's default error handling behavior
- **template**: `(string)`, no default — provide the template name that you want to render with `h.view(template, errorData)`
- **toTerminal**: `(boolean)`, default: `true` — print pretty errors to the terminal as well (enabled by default)
- **links**: `(array)`, defaults to Google and Stack Overflow icons that are linked with the error message as the search term (enabled by default). Pass an empty array `[]` to disable the default links

```js
await server.register({
plugin: require('hapi-dev-errors'),
options: {
showErrors: process.env.NODE_ENV !== 'production',
template: 'my-error-view',
toTerminal: true
toTerminal: true,
links: [ (error) => {
return `<a href="https://github.com/fs-opensource/hapi-dev-errors/search?q=${error.message}">
Search Youch on GitHub
</a>`
}
]
}
})

Expand Down
2 changes: 1 addition & 1 deletion examples/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ async function launchIt () {
method: 'GET',
path: '/{path*}',
handler: (request, h) => {
h.notAvailable()
return h.notAvailable()
}
})

Expand Down
44 changes: 44 additions & 0 deletions examples/with-links.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use strict'

const Hapi = require('hapi')

// create new server instance
// add server’s connection information
const server = new Hapi.Server({
host: 'localhost',
port: 3000
})

async function launchIt () {
await server.register({
plugin: require('../lib'),
options: {
showErrors: process.env.NODE_ENV !== 'production',
toTerminal: false,
links: [
(error) => {
return `<a rel="noopener noreferrer" target="_blank" href="https://github.com/fs-opensource/hapi-dev-errors/search?q=${error.message}">
Search Youch on GitHub
</a>`
}
]
}
})

server.route({
method: 'GET',
path: '/{path*}',
handler: (request, h) => {
h.notAvailable()
}
})

try {
await server.start()
console.log('Server running at: ' + server.info.uri)
} catch (err) {
throw err
}
}

launchIt()
79 changes: 66 additions & 13 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use strict'

const Hoek = require('hoek')
const Youch = require('youch')
const ForTerminal = require('youch-terminal')

Expand All @@ -14,19 +13,26 @@ const ForTerminal = require('youch-terminal')
*
* @returns {Object}
*/
function createYouch (request, error) {
// assign the url’s path to "url" property of request directly
// hapi uses a URL object and Youch wants the path directly
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

// assign httpVersion -> same as with request.url
request.httpVersion = request.raw.req.httpVersion

// let Youch show the error’s status code
error.status = error.output.statusCode

// pretty error printing on terminal or web view
return new Youch(error, request)
try {
const youch = new Youch(error, request)

links.forEach(link => youch.addLink(link))

return youch
} catch (error) {
console.error(error)
throw error
}
}

/**
Expand Down Expand Up @@ -55,6 +61,44 @@ 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 &quot;${error.message}&quot;">
<!-- 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 &quot;${error.message}&quot;">
<!-- 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.
*
Expand All @@ -64,7 +108,11 @@ function matches (str, regex) {
async function register (server, options) {
const defaults = {
showErrors: false,
toTerminal: true
toTerminal: true,
links: [
(error) => googleIcon(error),
(error) => stackOverflowIcon(error)
]
}

const config = Object.assign({}, defaults, options)
Expand All @@ -82,10 +130,15 @@ async function register (server, options) {
server.dependency(['vision'])
}

// Make sure the `links` are an array
if (!Array.isArray(config.links)) {
config.links = [config.links]
}

// extend the request lifecycle at `onPreResponse`
// to change the default error handling behavior (if enabled)
server.ext('onPreResponse', async (request, h) => {
const error = Hoek.clone(request.response)
const error = request.response

// only show `bad implementation` developer errors (status code 500)
if (error.isBoom && error.output.statusCode === 500) {
Expand All @@ -104,7 +157,7 @@ async function register (server, options) {
stacktrace: error.stack
}

const youch = createYouch(request, error)
const youch = createYouch({ request, error, links: config.links })

// print a pretty error to terminal as well
if (config.toTerminal) {
Expand Down
Binary file modified media/hapi-dev-errors-default-youch-view.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 4 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
"url": "https://github.com/fs-opensource/hapi-dev-errors/issues"
},
"dependencies": {
"hoek": "~5.0.3",
"youch": "~2.0.8",
"youch": "~2.0.10",
"youch-terminal": "~1.0.0"
},
"devDependencies": {
Expand All @@ -21,10 +20,10 @@
"eslint-plugin-promise": "~3.8.0",
"eslint-plugin-standard": "~3.1.0",
"hapi": "~17.6.0",
"husky": "~1.0.1",
"joi": "~13.6.0",
"husky": "~1.1.1",
"joi": "~13.7.0",
"lab": "~15.5.0",
"sinon": "~6.3.4",
"sinon": "~6.3.5",
"vision": "~5.4.0"
},
"engines": {
Expand Down
4 changes: 2 additions & 2 deletions test/plugin-falls-back-to-json.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ experiment('hapi-dev-error falls back to json', () => {
server.route(routeOptions)
})

test('test if the plugin responses json with json accept header', async () => {
test('test if the plugin responds json with json accept header', async () => {
const response = await server.inject({
url: '/error',
method: 'GET',
Expand All @@ -46,7 +46,7 @@ experiment('hapi-dev-error falls back to json', () => {
Code.expect(payload).to.startWith('{')
})

test('test if the plugin responses json with curl user-agent', async () => {
test('test if the plugin responds json with curl user-agent', async () => {
const response = await server.inject({
url: '/error',
method: 'GET',
Expand Down
100 changes: 100 additions & 0 deletions test/plugin-uses-links.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
'use strict'

const Lab = require('lab')
const Code = require('code')
const Hapi = require('hapi')
const Sinon = require('sinon')

const { experiment, test, beforeEach, afterEach } = (exports.lab = Lab.script())

experiment('hapi-dev-error handles custom user links', () => {
async function createServer (options) {
const server = new Hapi.Server()

await server.register({
plugin: require('../lib/index'),
options: {
showErrors: true,
toTerminal: false,
...options
}
})

const routeOptions = {
path: '/',
method: 'GET',
handler: () => new Error('Somethinng bad happened')
}

server.route(routeOptions)

return server
}

beforeEach(() => {
Sinon.stub(console, 'error')
})

afterEach(() => {
console.error.restore()
})

test('that the plugin works fine with empty links', async () => {
const server = await createServer({ links: [] })

const response = await server.inject({
url: '/',
method: 'GET'
})

Sinon.assert.notCalled(console.error)

Code.expect(response.statusCode).to.equal(500)
Code.expect(response.payload).to.startWith('<')
})

test('that the plugin throws if the links are strings', async () => {
const server = await createServer({ links: [ 'error' ] })

const response = await server.inject({
url: '/',
method: 'GET'
})

Sinon.assert.called(console.error)

Code.expect(response.statusCode).to.equal(500)
Code.expect(response.payload).to.startWith('{')
Code.expect(response.payload).to.include('Internal Server Error')
})

test('that the plugin throws if the links is not an array of functions', async () => {
const server = await createServer({ links: 'error' })

const response = await server.inject({
url: '/',
method: 'GET'
})

Sinon.assert.called(console.error)

Code.expect(response.statusCode).to.equal(500)
Code.expect(response.payload).to.startWith('{')
Code.expect(response.payload).to.include('Internal Server Error')
})

test('that the plugin works fine with a link function', async () => {
const server = await createServer({ links: () => `link` })

const response = await server.inject({
url: '/',
method: 'GET',
headers: { accept: 'application/json' }
})

Sinon.assert.notCalled(console.error)

Code.expect(response.statusCode).to.equal(500)
Code.expect(response.payload).to.startWith('{')
})
})