diff --git a/README.md b/README.md
index ba80cb2..eba021b 100644
--- a/README.md
+++ b/README.md
@@ -103,6 +103,7 @@ 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({
@@ -110,7 +111,13 @@ await server.register({
options: {
showErrors: process.env.NODE_ENV !== 'production',
template: 'my-error-view',
- toTerminal: true
+ toTerminal: true,
+ links: [ (error) => {
+ return `
+ Search Youch on GitHub
+ `
+ }
+ ]
}
})
diff --git a/examples/default.js b/examples/default.js
index 82e396d..a440b77 100644
--- a/examples/default.js
+++ b/examples/default.js
@@ -21,7 +21,7 @@ async function launchIt () {
method: 'GET',
path: '/{path*}',
handler: (request, h) => {
- h.notAvailable()
+ return h.notAvailable()
}
})
diff --git a/examples/with-links.js b/examples/with-links.js
new file mode 100644
index 0000000..6a95739
--- /dev/null
+++ b/examples/with-links.js
@@ -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 `
+ Search Youch on GitHub
+ `
+ }
+ ]
+ }
+ })
+
+ 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()
diff --git a/lib/index.js b/lib/index.js
index 0cb607c..fe9d272 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -1,6 +1,5 @@
'use strict'
-const Hoek = require('hoek')
const Youch = require('youch')
const ForTerminal = require('youch-terminal')
@@ -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
+ }
}
/**
@@ -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 `
+
+
+
+ `
+}
+
+/**
+ * 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 `
+
+
+
+ `
+}
+
/**
* Render better error views during development.
*
@@ -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)
@@ -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) {
@@ -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) {
diff --git a/media/hapi-dev-errors-default-youch-view.png b/media/hapi-dev-errors-default-youch-view.png
index 08088b9..d6b4068 100644
Binary files a/media/hapi-dev-errors-default-youch-view.png and b/media/hapi-dev-errors-default-youch-view.png differ
diff --git a/package.json b/package.json
index af335c6..12484d7 100644
--- a/package.json
+++ b/package.json
@@ -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": {
@@ -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": {
diff --git a/test/plugin-falls-back-to-json.js b/test/plugin-falls-back-to-json.js
index eb7e35c..ae26972 100644
--- a/test/plugin-falls-back-to-json.js
+++ b/test/plugin-falls-back-to-json.js
@@ -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',
@@ -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',
diff --git a/test/plugin-uses-links.js b/test/plugin-uses-links.js
new file mode 100644
index 0000000..141468d
--- /dev/null
+++ b/test/plugin-uses-links.js
@@ -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('{')
+ })
+})