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 = 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 = ` Sorry, an unexpected error occurred while processing your request.
${suggestion ? '
Suggestion: ' + suggestion + '
' : ''}

Name: ${thrownError.name}
Message: ${thrownError.message}
Stack trace:
    - ${thrownError.stack ? thrownError.stack.split(/\s+at\s+/).slice(1)
        .join('
- ') : 'none'}
` if ( context && typeof context === 'object' ) { str += `

Context:
${Object.keys(context).map(key => `    - ${key} : ${JSON.stringify(context[key]).replace(/\n/g, '
')}`) .join('\n')}
` } 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 '' } }