Skip to content

Commit 7324e4c

Browse files
authored
Merge pull request #7 from marcuspoehls/helpful-links
Add helpful links: linked SVG icons to Google and Stack Overflow
2 parents c83c6f5 + 7bf79a2 commit 7324e4c

8 files changed

Lines changed: 225 additions & 22 deletions

File tree

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,14 +103,21 @@ The following plugin options allow you to customize the default behavior of `hap
103103
- **showErrors**: `(boolean)`, default: `false` — by default, the plugin is disabled and keeps hapi's default error handling behavior
104104
- **template**: `(string)`, no default — provide the template name that you want to render with `h.view(template, errorData)`
105105
- **toTerminal**: `(boolean)`, default: `true` — print pretty errors to the terminal as well (enabled by default)
106+
- **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
106107

107108
```js
108109
await server.register({
109110
plugin: require('hapi-dev-errors'),
110111
options: {
111112
showErrors: process.env.NODE_ENV !== 'production',
112113
template: 'my-error-view',
113-
toTerminal: true
114+
toTerminal: true,
115+
links: [ (error) => {
116+
return `<a href="https://github.com/fs-opensource/hapi-dev-errors/search?q=${error.message}">
117+
Search Youch on GitHub
118+
</a>`
119+
}
120+
]
114121
}
115122
})
116123

examples/default.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ async function launchIt () {
2121
method: 'GET',
2222
path: '/{path*}',
2323
handler: (request, h) => {
24-
h.notAvailable()
24+
return h.notAvailable()
2525
}
2626
})
2727

examples/with-links.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
'use strict'
2+
3+
const Hapi = require('hapi')
4+
5+
// create new server instance
6+
// add server’s connection information
7+
const server = new Hapi.Server({
8+
host: 'localhost',
9+
port: 3000
10+
})
11+
12+
async function launchIt () {
13+
await server.register({
14+
plugin: require('../lib'),
15+
options: {
16+
showErrors: process.env.NODE_ENV !== 'production',
17+
toTerminal: false,
18+
links: [
19+
(error) => {
20+
return `<a rel="noopener noreferrer" target="_blank" href="https://github.com/fs-opensource/hapi-dev-errors/search?q=${error.message}">
21+
Search Youch on GitHub
22+
</a>`
23+
}
24+
]
25+
}
26+
})
27+
28+
server.route({
29+
method: 'GET',
30+
path: '/{path*}',
31+
handler: (request, h) => {
32+
h.notAvailable()
33+
}
34+
})
35+
36+
try {
37+
await server.start()
38+
console.log('Server running at: ' + server.info.uri)
39+
} catch (err) {
40+
throw err
41+
}
42+
}
43+
44+
launchIt()

lib/index.js

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
'use strict'
22

3-
const Hoek = require('hoek')
43
const Youch = require('youch')
54
const ForTerminal = require('youch-terminal')
65

@@ -14,19 +13,26 @@ const ForTerminal = require('youch-terminal')
1413
*
1514
* @returns {Object}
1615
*/
17-
function createYouch (request, error) {
18-
// assign the url’s path to "url" property of request directly
19-
// hapi uses a URL object and Youch wants the path directly
16+
function createYouch ({ request, error, links = [] }) {
17+
/**
18+
* hapi’s request and error objects don’t match the
19+
* expected structure in Youch. We need to adjust
20+
* properties to display them correctly.
21+
*/
2022
request.url = request.path
21-
22-
// assign httpVersion -> same as with request.url
2323
request.httpVersion = request.raw.req.httpVersion
24-
25-
// let Youch show the error’s status code
2624
error.status = error.output.statusCode
2725

28-
// pretty error printing on terminal or web view
29-
return new Youch(error, request)
26+
try {
27+
const youch = new Youch(error, request)
28+
29+
links.forEach(link => youch.addLink(link))
30+
31+
return youch
32+
} catch (error) {
33+
console.error(error)
34+
throw error
35+
}
3036
}
3137

3238
/**
@@ -55,6 +61,44 @@ function matches (str, regex) {
5561
return str && str.match(regex)
5662
}
5763

64+
/**
65+
* Returns a link to Google that includes
66+
* the error message as the search
67+
* term. The link is an SVG icon.
68+
*
69+
* @param {Object} error
70+
*
71+
* @returns {String}
72+
*/
73+
function googleIcon (error) {
74+
return `<a rel="noopener noreferrer" target="_blank" href="https://google.com/search?q=${encodeURIComponent(error.message)}" title="Search Google for &quot;${error.message}&quot;">
75+
<!-- Google icon by Picons.me, found at https://www.iconfinder.com/Picons -->
76+
<!-- Free for commercial use -->
77+
<svg width="24" height="24" viewBox="0 0 56.6934 56.6934" xmlns="http://www.w3.org/2000/svg">
78+
<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"/>
79+
</svg>
80+
</a>`
81+
}
82+
83+
/**
84+
* Returns a link to Stack Overflow that
85+
* includes the error message as the
86+
* search term. The link is an SVG icon.
87+
*
88+
* @param {Object} error
89+
*
90+
* @returns {String}
91+
*/
92+
function stackOverflowIcon (error) {
93+
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;">
94+
<!-- Stack Overflow icon by Picons.me, found at https://www.iconfinder.com/Picons -->
95+
<!-- Free for commercial use -->
96+
<svg width="24" height="24" viewBox="-1163 1657.697 56.693 56.693" xmlns="http://www.w3.org/2000/svg">
97+
<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"/>
98+
</svg>
99+
</a>`
100+
}
101+
58102
/**
59103
* Render better error views during development.
60104
*
@@ -64,7 +108,11 @@ function matches (str, regex) {
64108
async function register (server, options) {
65109
const defaults = {
66110
showErrors: false,
67-
toTerminal: true
111+
toTerminal: true,
112+
links: [
113+
(error) => googleIcon(error),
114+
(error) => stackOverflowIcon(error)
115+
]
68116
}
69117

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

133+
// Make sure the `links` are an array
134+
if (!Array.isArray(config.links)) {
135+
config.links = [config.links]
136+
}
137+
85138
// extend the request lifecycle at `onPreResponse`
86139
// to change the default error handling behavior (if enabled)
87140
server.ext('onPreResponse', async (request, h) => {
88-
const error = Hoek.clone(request.response)
141+
const error = request.response
89142

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

107-
const youch = createYouch(request, error)
160+
const youch = createYouch({ request, error, links: config.links })
108161

109162
// print a pretty error to terminal as well
110163
if (config.toTerminal) {
936 Bytes
Loading

package.json

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@
77
"url": "https://github.com/fs-opensource/hapi-dev-errors/issues"
88
},
99
"dependencies": {
10-
"hoek": "~5.0.3",
11-
"youch": "~2.0.8",
10+
"youch": "~2.0.10",
1211
"youch-terminal": "~1.0.0"
1312
},
1413
"devDependencies": {
@@ -21,10 +20,10 @@
2120
"eslint-plugin-promise": "~3.8.0",
2221
"eslint-plugin-standard": "~3.1.0",
2322
"hapi": "~17.6.0",
24-
"husky": "~1.0.1",
25-
"joi": "~13.6.0",
23+
"husky": "~1.1.1",
24+
"joi": "~13.7.0",
2625
"lab": "~15.5.0",
27-
"sinon": "~6.3.4",
26+
"sinon": "~6.3.5",
2827
"vision": "~5.4.0"
2928
},
3029
"engines": {

test/plugin-falls-back-to-json.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ experiment('hapi-dev-error falls back to json', () => {
3232
server.route(routeOptions)
3333
})
3434

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

49-
test('test if the plugin responses json with curl user-agent', async () => {
49+
test('test if the plugin responds json with curl user-agent', async () => {
5050
const response = await server.inject({
5151
url: '/error',
5252
method: 'GET',

test/plugin-uses-links.js

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
'use strict'
2+
3+
const Lab = require('lab')
4+
const Code = require('code')
5+
const Hapi = require('hapi')
6+
const Sinon = require('sinon')
7+
8+
const { experiment, test, beforeEach, afterEach } = (exports.lab = Lab.script())
9+
10+
experiment('hapi-dev-error handles custom user links', () => {
11+
async function createServer (options) {
12+
const server = new Hapi.Server()
13+
14+
await server.register({
15+
plugin: require('../lib/index'),
16+
options: {
17+
showErrors: true,
18+
toTerminal: false,
19+
...options
20+
}
21+
})
22+
23+
const routeOptions = {
24+
path: '/',
25+
method: 'GET',
26+
handler: () => new Error('Somethinng bad happened')
27+
}
28+
29+
server.route(routeOptions)
30+
31+
return server
32+
}
33+
34+
beforeEach(() => {
35+
Sinon.stub(console, 'error')
36+
})
37+
38+
afterEach(() => {
39+
console.error.restore()
40+
})
41+
42+
test('that the plugin works fine with empty links', async () => {
43+
const server = await createServer({ links: [] })
44+
45+
const response = await server.inject({
46+
url: '/',
47+
method: 'GET'
48+
})
49+
50+
Sinon.assert.notCalled(console.error)
51+
52+
Code.expect(response.statusCode).to.equal(500)
53+
Code.expect(response.payload).to.startWith('<')
54+
})
55+
56+
test('that the plugin throws if the links are strings', async () => {
57+
const server = await createServer({ links: [ 'error' ] })
58+
59+
const response = await server.inject({
60+
url: '/',
61+
method: 'GET'
62+
})
63+
64+
Sinon.assert.called(console.error)
65+
66+
Code.expect(response.statusCode).to.equal(500)
67+
Code.expect(response.payload).to.startWith('{')
68+
Code.expect(response.payload).to.include('Internal Server Error')
69+
})
70+
71+
test('that the plugin throws if the links is not an array of functions', async () => {
72+
const server = await createServer({ links: 'error' })
73+
74+
const response = await server.inject({
75+
url: '/',
76+
method: 'GET'
77+
})
78+
79+
Sinon.assert.called(console.error)
80+
81+
Code.expect(response.statusCode).to.equal(500)
82+
Code.expect(response.payload).to.startWith('{')
83+
Code.expect(response.payload).to.include('Internal Server Error')
84+
})
85+
86+
test('that the plugin works fine with a link function', async () => {
87+
const server = await createServer({ links: () => `link` })
88+
89+
const response = await server.inject({
90+
url: '/',
91+
method: 'GET',
92+
headers: { accept: 'application/json' }
93+
})
94+
95+
Sinon.assert.notCalled(console.error)
96+
97+
Code.expect(response.statusCode).to.equal(500)
98+
Code.expect(response.payload).to.startWith('{')
99+
})
100+
})

0 commit comments

Comments
 (0)