Skip to content

Commit 32357c4

Browse files
committed
refactor to class
1 parent d0b2df3 commit 32357c4

File tree

4 files changed

+189
-171
lines changed

4 files changed

+189
-171
lines changed

lib/error-handler.js

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
'use strict'
2+
3+
const Fs = require('fs')
4+
const Path = require('path')
5+
const Youch = require('youch')
6+
const ForTerminal = require('youch-terminal')
7+
8+
class ErrorHandler {
9+
constructor (options) {
10+
const {
11+
toTerminal = true,
12+
template,
13+
links = this.defaultLinks()
14+
} = options
15+
16+
this.template = template
17+
this.toTerminal = toTerminal
18+
this.links = Array.isArray(links) ? links : [links]
19+
}
20+
21+
defaultLinks () {
22+
return [
23+
(error) => this.google(error),
24+
(error) => this.stackOverflow(error)
25+
]
26+
}
27+
28+
google (error) {
29+
return `
30+
<a
31+
rel="noopener noreferrer" target="_blank"
32+
href="https://google.com/search?q=${encodeURIComponent(error.message)}"
33+
title="Search Google for &quot;${error.message}&quot;">
34+
${this.resolveIcon('google.svg')}
35+
</a>`
36+
}
37+
38+
stackOverflow (error) {
39+
return `
40+
<a
41+
rel="noopener noreferrer" target="_blank"
42+
href="https://stackoverflow.com/search?q=${encodeURIComponent(error.message)}"
43+
title="Search Stack Overflow for &quot;${error.message}&quot;">
44+
${this.resolveIcon('stackoverflow.svg')}
45+
</a>`
46+
}
47+
48+
resolveIcon (name) {
49+
return Fs.readFileSync(this.resolveIconPath(name))
50+
}
51+
52+
resolveIconPath (name) {
53+
return Path.resolve(__dirname, 'icons', name)
54+
}
55+
56+
async handle (request, h) {
57+
if (this.isDeveloperError(request.response)) {
58+
return this.resolveError(request, h)
59+
}
60+
61+
return h.continue
62+
}
63+
64+
isDeveloperError (error) {
65+
return error.isBoom && error.output.statusCode === 500
66+
}
67+
68+
async resolveError (request, h) {
69+
await this.logToTerminal(request)
70+
71+
if (this.wantsJson(request)) {
72+
return this.sendJson(request, h)
73+
}
74+
75+
if (this.hasTemplate()) {
76+
return this.renderTemplate(request, h)
77+
}
78+
79+
return this.sendHtml(request, h)
80+
}
81+
82+
async logToTerminal (request) {
83+
if (this.toTerminal) {
84+
const youch = this.createYouch(request)
85+
const json = await youch.toJSON()
86+
87+
console.log(ForTerminal(json))
88+
}
89+
}
90+
91+
createYouch (request) {
92+
const error = request.response
93+
error.status = error.output.statusCode
94+
95+
try {
96+
const youch = new Youch(error, request.raw.req)
97+
98+
this.links.forEach(link => youch.addLink(link))
99+
100+
return youch
101+
} catch (error) {
102+
console.error(error)
103+
throw error
104+
}
105+
}
106+
107+
wantsJson (request) {
108+
const { 'user-agent': agent, accept } = request.raw.req.headers
109+
110+
return this.matches(agent, /curl|wget|postman|insomnia/i) || this.matches(accept, /json/)
111+
}
112+
113+
matches (str, regex) {
114+
return str && str.match(regex)
115+
}
116+
117+
composeError (request) {
118+
const error = request.response
119+
120+
return {
121+
title: error.output.payload.error,
122+
message: error.message,
123+
statusCode: error.output.statusCode,
124+
url: request.url.path,
125+
method: request.raw.req.method,
126+
headers: request.raw.req.headers,
127+
payload: request.raw.req.method !== 'GET' ? request.payload : '',
128+
stacktrace: error.stack
129+
}
130+
}
131+
132+
sendJson (request, h) {
133+
const error = this.composeError(request)
134+
const json = this.resolveJson(error)
135+
136+
return h
137+
.response(json)
138+
.type('application/json')
139+
.code(error.statusCode)
140+
}
141+
142+
resolveJson (data) {
143+
return JSON.stringify({
144+
...data,
145+
stacktrace: data.stacktrace.split('\n').map(line => line.trim())
146+
})
147+
}
148+
149+
hasTemplate () {
150+
return !!this.template
151+
}
152+
153+
renderTemplate (request, h) {
154+
const error = this.composeError(request)
155+
156+
return h
157+
.view(this.template, { request, error: request.response, ...error })
158+
.code(error.statusCode)
159+
}
160+
161+
async sendHtml (request, h) {
162+
const youch = this.createYouch(request)
163+
const statusCode = request.response.output.statusCode
164+
165+
return h
166+
.response(await youch.toHTML())
167+
.type('text/html')
168+
.code(statusCode)
169+
}
170+
}
171+
172+
module.exports = ErrorHandler

lib/icons/google.svg

Lines changed: 5 additions & 0 deletions
Loading

lib/icons/stackoverflow.svg

Lines changed: 5 additions & 0 deletions
Loading

lib/index.js

Lines changed: 7 additions & 171 deletions
Original file line numberDiff line numberDiff line change
@@ -1,186 +1,22 @@
11
'use strict'
22

3-
const Youch = require('youch')
4-
const ForTerminal = require('youch-terminal')
3+
const ErrorHandler = require('./error-handler')
54

6-
/**
7-
* Create a Youch instance for pretty error printing.
8-
* This instance is used to format output for the
9-
* console and for a web view.
10-
*
11-
* @param {Object} request - the request object
12-
* @param {Object} error - object with error details
13-
*
14-
* @returns {Object}
15-
*/
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-
*/
22-
request.url = request.path
23-
request.httpVersion = request.raw.req.httpVersion
24-
error.status = error.output.statusCode
25-
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-
}
36-
}
37-
38-
/**
39-
* Check whether the incoming request requires a JSON response.
40-
* This is true for requests where the "accept" header
41-
* contains "json" or the agent is a CLI/GUI app.
42-
*
43-
* @param {Object}
44-
*
45-
* @returns {Boolean}
46-
*/
47-
function wantsJson ({ agent, accept }) {
48-
return matches(agent, /curl|wget|postman|insomnia/i) || matches(accept, /json/)
49-
}
50-
51-
/**
52-
* Helper function to test whether a given
53-
* string matches a RegEx.
54-
*
55-
* @param {String} str
56-
* @param {String} regex
57-
*
58-
* @returns {Boolean}
59-
*/
60-
function matches (str, regex) {
61-
return str && str.match(regex)
62-
}
63-
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-
102-
/**
103-
* Render better error views during development.
104-
*
105-
* @param {Object} server - hapi server instance where the plugin is registered
106-
* @param {Object} options - plugin options
107-
*/
1085
async function register (server, options) {
109-
const defaults = {
110-
showErrors: false,
111-
toTerminal: true,
112-
links: [
113-
(error) => googleIcon(error),
114-
(error) => stackOverflowIcon(error)
115-
]
116-
}
117-
118-
const config = Object.assign({}, defaults, options)
119-
config.links = Array.isArray(config.links) ? config.links : [config.links]
6+
const { showErrors = false, template, ...config } = options
1207

121-
if (!config.showErrors) {
8+
if (!showErrors) {
1229
return
12310
}
12411

125-
if (config.template) {
12+
if (template) {
12613
server.dependency(['vision'])
12714
}
12815

129-
server.ext('onPreResponse', async (request, h) => {
130-
const error = request.response
131-
132-
if (error.isBoom && error.output.statusCode === 500) {
133-
const accept = request.raw.req.headers.accept
134-
const agent = request.raw.req.headers['user-agent']
135-
const statusCode = error.output.statusCode
136-
137-
const errorResponse = {
138-
title: error.output.payload.error,
139-
statusCode,
140-
message: error.message,
141-
method: request.raw.req.method,
142-
url: request.url.path,
143-
headers: request.raw.req.headers,
144-
payload: request.raw.req.method !== 'GET' ? request.payload : '',
145-
stacktrace: error.stack
146-
}
147-
148-
const youch = createYouch({ request, error, links: config.links })
149-
150-
// print a pretty error to terminal as well
151-
if (config.toTerminal) {
152-
const json = await youch.toJSON()
153-
console.log(ForTerminal(json))
154-
}
16+
const errorHandler = new ErrorHandler({ template, ...config })
15517

156-
if (wantsJson({ accept, agent })) {
157-
const details = Object.assign({}, errorResponse, {
158-
stacktrace: errorResponse.stacktrace.split('\n').map(line => line.trim())
159-
})
160-
161-
return h
162-
.response(JSON.stringify(details, null, 2))
163-
.type('application/json')
164-
.code(statusCode)
165-
}
166-
167-
// did the user explicitly specify an error template
168-
// favor a user’s custom template over the default template
169-
if (config.template) {
170-
return h
171-
.view(config.template, { request, error, ...errorResponse })
172-
.code(statusCode)
173-
}
174-
175-
const html = await youch.toHTML()
176-
177-
return h
178-
.response(html)
179-
.type('text/html')
180-
.code(statusCode)
181-
}
182-
183-
return h.continue
18+
server.ext('onPreResponse', async (request, h) => {
19+
return errorHandler.handle(request, h)
18420
})
18521
}
18622

0 commit comments

Comments
 (0)