You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
lib/src/http/response/ErrorResponseFactory.ts

121 lines
4.1 KiB

import {ResponseFactory} from './ResponseFactory'
import {ErrorWithContext, HTTPStatus} from '../../util'
import {Request} from '../lifecycle/Request'
import * as api from './api'
/**
* Helper to create a new ErrorResponseFactory, with the given HTTP status and output format.
* @param thrownError
* @param status
* @param output
*/
export function error(
thrownError: Error | string,
status: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR,
output: 'json' | 'html' | 'auto' = 'auto',
): ErrorResponseFactory {
if ( typeof thrownError === 'string' ) {
thrownError = new Error(thrownError)
}
return new ErrorResponseFactory(thrownError, status, output)
}
/**
* Response factory that renders an Error object to the client in a specified format.
*/
export class ErrorResponseFactory extends ResponseFactory {
protected targetMode: 'json' | 'html' | 'auto' = 'auto'
constructor(
public readonly thrownError: Error,
status: HTTPStatus,
output: 'json' | 'html' | 'auto' = 'auto',
) {
super()
this.status(status)
this.mode(output)
}
public mode(output: 'json' | 'html' | 'auto'): ErrorResponseFactory {
this.targetMode = output
return this
}
public async write(request: Request): Promise<Request> {
request = await super.write(request)
const wants = request.wants()
if ( this.targetMode === 'json' || (this.targetMode === 'auto' && wants === 'json') ) {
request.response.setHeader('Content-Type', 'application/json')
request.response.body = this.buildJSON(this.thrownError)
} else if ( this.targetMode === 'html' || (this.targetMode === 'auto' && (wants === 'html' || wants === 'unknown')) ) {
request.response.setHeader('Content-Type', 'text/html')
request.response.body = this.buildHTML(this.thrownError)
}
// FIXME XML support
return request
}
/**
* Build the HTML display for the given error.
* @param {Error} error
* @return string
*/
protected buildHTML(thrownError: Error): string {
let context: any
if ( thrownError instanceof ErrorWithContext ) {
context = thrownError.context
if ( thrownError.originalError ) {
thrownError = thrownError.originalError
}
}
const suggestion = this.getSuggestion()
let str = `
<b>Sorry, an unexpected error occurred while processing your request.</b>
<br>
${suggestion ? '<br><b>Suggestion:</b> ' + suggestion + '<br>' : ''}
<pre><code>
Name: ${thrownError.name}
Message: ${thrownError.message}
Stack trace:
- ${thrownError.stack ? thrownError.stack.split(/\s+at\s+/).slice(1)
.join('<br> - ') : 'none'}
</code></pre>
`
if ( context && typeof context === 'object' ) {
str += `
<pre><code>
Context:
${Object.keys(context).map(key => ` - ${key} : ${JSON.stringify(context[key]).replace(/\n/g, '<br>')}`)
.join('\n')}
</code></pre>
`
}
return str
}
protected buildJSON(thrownError: Error): string {
return JSON.stringify(api.error(thrownError))
}
protected getSuggestion(): string {
if ( this.thrownError.message.startsWith('No such dependency is registered with this container: class SecurityContext') ) {
return 'It looks like this route relies on the security framework. Is the route you are accessing inside a middleware (e.g. SessionAuthMiddleware)?'
} else if ( this.thrownError.message.startsWith('Unable to resolve schema for validator') ) {
return 'Make sure the directory in which the interface file is located is listed in extollo.cc.zodify in package.json, and that it ends with the proper .type.ts suffix.'
} else if ( this.thrownError instanceof ErrorWithContext ) {
if ( typeof this.thrownError.context.suggestion === 'string' ) {
return this.thrownError.context.suggestion
}
}
return ''
}
}