Node.js und RFC-konforme Fehlermeldung
Bei der Entwicklung von Schnittstellen stellt sich die Frage: „Wie gehe ich mit Fehlern um?“ Meine Antwort lautet: „Möglichst an zentraler Stelle und standardkonform.“ Ich zeige euch, welcher Ansatz sich für mich im Node.js-Umfeld bewährt hat. Fangen wir an!
In diesem Artikel zeige ich meinen Ansatz, Schnittstellenfehler der Kategorie „Invalid Parameter“ standardkonform auszugeben. Hierzu berücksichtige ich RFC 7807, in dem Problemdetails maschinenlesbar als HTTP-Response transportiert werden. Die Voraussetzungen hierfür sind die Einrichtung von Node.js mit TSOA, die Typendefinition in Node.js mit TSOA inklusive der Request-Body-Validierung sowie die Implementierung meiner Base-Error-Klasse.
Für die Umsetzung sind folgende Schritte notwendig:
- Die Definition der passenden Klasse für den „InvalidParams“-Error.
- Das Mapping der Error-Klassen in ein RFC-konformes Problemdokument.
- Die Erstellung einer Error-Middleware für Express.
- Das Einbinden in die Express-Applikation.
1. Der InvalidParams-Error
Wir stellen Detailinformationen zu den invaliden Parametern bereit, damit wir später in der UI anzeigen können, welche Eingabe zu einem Problem geführt hat (z. B. in Form einer Fehlermeldung und einer roten Umrandung des Eingabefelds).
// Defining the structure of validation errors in analogy to TSOA
export interface ValidationErrors {
[name: string]: {
message: string // The message shown in the frontend
received?: unknown // The value we received in the API, if available
expected?: unknown // The value we expected, if given
code?: string // To give a hint about what we expect as a machine-readable value
}
}
// Extending BaseError with the newly defined structure
interface InvalidParamsErrorOptions extends BaseErrorOptions {
validationResult: ValidationErrors
}
Die neue Klasse erweitert den BaseError entsprechend der oben genannten Typendefinition.
class InvalidParamsError extends BaseError {
public validationResult: ValidationErrors
constructor(options: InvalidParamsErrorOptions) {
const { detail = 'Invalid parameters', cause, validationResult } = options
super({ detail, cause, statusCode: 422 })
this.validationResult = validationResult
}
public toJSON() {
return {
...super.toJSON(),
validationResult: this.validationResult,
}
}
public toString() {
return JSON.stringify(this.toJSON())
}
}
2. Das Mapping in ein RFC-konformes Problemdokument
Für die grundsätzliche Funktionalität sind zwei passende NPM-Pakete vorhanden:
- NPM-Paket http-problem-details stellt ein
ProblemDocumentbereit. Es generiert das notwendigeproblem+json-Dokument. - NPM-Paket http-problem-details-mapper ermöglicht uns, eine Mapping-Middleware basierend auf dem
ProblemDocumentzu bauen.
npm install http-problem-details http-problem-details-mapper
Mithilfe der beiden NPM-Pakete definieren wir das Mapping zum Problemdokument. Dabei stellen wir sicher, dass der Statuscode und die Fehlernachricht im Feld status bzw. detail geschrieben sind.
import { ProblemDocument } from 'http-problem-details'
import { ErrorMapper } from 'http-problem-details-mapper'
export class InvalidParamsMapper extends ErrorMapper {
mapError(error: InvalidParamsError) {
return new ProblemDocument(
{
status: error.statusCode,
title: 'Invalid parameter',
type: 'validation-error',
detail: error.detail,
},
{
invalidParameter: error.validationResult,
},
)
}
}
3. Die notwendigen Error-Middleware-Implementierungen
Für die Einbindung in die Express-Applikation fehlen uns noch zwei Bausteine. Zum einen den Baustein, der den TSOA-Fehler in unsere InvalidParams-Error-Klasse umwandelt.
import { ErrorRequestHandler, NextFunction, Request, Response } from 'express'
import { ValidateError } from 'tsoa'
import InvalidParamsError from '@/error/InvalidParamsError'
const validationErrorMiddleware: ErrorRequestHandler = (
err: unknown,
_req: Request,
_res: Response,
next: NextFunction,
) => {
if (err instanceof ValidateError) {
next(new InvalidParamsError({ detail: 'Validation failed', validationResult: err.fields }))
}
next(err)
}
export default validationErrorMiddleware
Zum anderen den Baustein, der die Error-Klasse in die gewünschte Http-Response umwandelt.
import { ErrorRequestHandler, NextFunction, Request, Response } from 'express'
import { IMappingStrategy } from 'http-problem-details-mapper'
interface HttpProblemResponseOptions {
strategy: IMappingStrategy
}
function HttpProblemResponse(options: HttpProblemResponseOptions): ErrorRequestHandler {
const { strategy } = options
return function HttpProblemDetailsMiddleware(
error: Error,
_request: Request,
response: Response,
next: NextFunction,
): void {
const problem = strategy.map(error)
// in case we cannot map the error, we will not handle it
if (!problem) {
next(error)
return
}
response.statusCode = problem.status
response.setHeader('Content-Type', 'application/problem+json')
response.json(problem)
}
}
export default HttpProblemResponse
4. Einbinden in die Express-Applikation
Als letzten Schritt binden wir alle Teile in die Express-Applikation ein:
- Wir erstellen das Mapping für den InvalidParams-Error-Mapper, damit er die Error-Klasse in
ProblemDocumentübersetzt. - Wir binden die
validationErrorMiddlewareein, die den TSOA-Fehler in einen InvalidParams-Error umwandelt, sodass das Mapping greift. - Wir aktivieren die Übersetzung des Mappers.
// Set up mapping to ensure translation to ProblemDocument
const mapperRegistry = new MapperRegistry()
.registerMapper(new InvalidParamsMapper())
// here you register other errors you want to map
const ErrorMappingStrategy = new DefaultMappingStrategy(mapperRegistry)
// This translates the tsoa validation error into an InvalidParamsError,
// which gets resolved into an RFC error response in the last handler
app.use(validationErrorMiddleware)
// The RFC 7807 error middleware has to be the last app.use() to work as intended
// It will map the thrown errors to the RFC applying responses
app.use(HttpProblemResponse({ strategy: ErrorMappingStrategy }))
Fazit
Der Artikel zeigt, dass einige Teile zusammengeführt werden, um das Ziel zu erreichen. Mit dieser Struktur haben wir die Plattform gebaut, um weitere Fehler zu definieren und sie zentral in eine RFC-konforme HTTP-Response zu übersetzen. Beispiele wären UnauthenticatedError oder UnauthorizedError, die ich in den nachfolgenden Artikeln zum Thema Node.js + TSOA + Authentifizierung aufgreife. Genauso lässt sich der NotFoundError nennen, der im Zusammenspiel mit Prisma wichtig ist.
Wenn du wissen willst, wie ich das Ganze in einem Projekt anwende und wie ich meinen Code organisiere, dann
Lass uns zusammen auf den Code schauen