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!

4 Minuten

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:

  1. Die Definition der passenden Klasse für den „InvalidParams“-Error.
  2. Das Mapping der Error-Klassen in ein RFC-konformes Problemdokument.
  3. Die Erstellung einer Error-Middleware für Express.
  4. 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 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 validationErrorMiddleware ein, 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
call to action background image

Abonniere meinen Newsletter

Erhalte einmal im Monat Nachrichten aus den Bereichen Softwareentwicklung und Kommunikation gespikt mit Buch- und Linkempfehlungen.