67 lines
2.9 KiB
TypeScript
67 lines
2.9 KiB
TypeScript
import type {} from '@fastify/static'
|
|
import type { FastifyError, FastifyReply, FastifyRequest } from 'fastify'
|
|
import { existsSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import type { Log } from './log.ts'
|
|
|
|
/**
|
|
* An error answered with its status code and message as { error }. A refused write also carries the problems by
|
|
* field and a message about what it would break elsewhere, answered as { error, problems, message }.
|
|
*/
|
|
export class HttpError extends Error {
|
|
statusCode: number
|
|
problems?: Record<string, string>
|
|
detail?: string
|
|
|
|
constructor(statusCode: number, message: string, problems?: Record<string, string>, detail?: string) {
|
|
super(message)
|
|
this.statusCode = statusCode
|
|
this.problems = problems
|
|
this.detail = detail
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A JSON schema of an object with exactly the properties given, all required. With Fastify's removeAdditional off, a
|
|
* body with another property is refused with 400.
|
|
*/
|
|
export function strict(properties: Record<string, unknown>) {
|
|
return { type: 'object', required: Object.keys(properties), additionalProperties: false, properties }
|
|
}
|
|
|
|
/**
|
|
* The error handler of setErrorHandler: the status of the error, 500 when it has none, with { error }, and the
|
|
* problems and message of an HttpError that has problems. An error of 500 or more that is not an HttpError is logged
|
|
* with its stack. A request that fails its schema is answered with "The request is not valid".
|
|
*/
|
|
export function errorHandler(log: Log) {
|
|
return (error: FastifyError, request: FastifyRequest, reply: FastifyReply) => {
|
|
const status = error.statusCode ?? 500
|
|
if (status >= 500 && !(error instanceof HttpError)) {
|
|
log.error(`${request.method} ${request.url}: ${error.stack ?? error.message}`)
|
|
}
|
|
const message = error.validation ? `The request is not valid (${error.message}).` : error.message
|
|
if (error instanceof HttpError && error.problems !== undefined) {
|
|
return reply.code(status).send({ error: message, problems: error.problems, message: error.detail ?? '' })
|
|
}
|
|
return reply.code(status).send({ error: message })
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The not-found handler of setNotFoundHandler for an application of webDir, the built frontend served by
|
|
* @fastify/static: a path under /api is 404 with { error }; any other path answers index.html, so the frontend's
|
|
* routes work on a reload, or 503 with buildHint while the frontend is not built.
|
|
*/
|
|
export function notFoundHandler(webDir: string, buildHint: string) {
|
|
return (request: FastifyRequest, reply: FastifyReply) => {
|
|
if (request.url === '/api' || request.url.startsWith('/api/')) {
|
|
return reply.code(404).send({ error: `'${request.method} ${request.url.split('?')[0]}' is not an endpoint of this tool.` })
|
|
}
|
|
if (!existsSync(join(webDir, 'index.html'))) {
|
|
return reply.code(503).type('text/plain; charset=utf-8').send(`${buildHint}\n`)
|
|
}
|
|
return reply.type('text/html; charset=utf-8').sendFile('index.html')
|
|
}
|
|
}
|