Fehlerbehandlung & Fallbacks
Drei Schichten verwandeln einen geworfenen Fehler in eine Response, von
innen nach außen: ein gescoptes handleErrors, dann das serverweite
withErrorHandler, dann der Framework-Default (HttpError → sein Status +
{ error, … }-JSON, alles andere → ein generischer 500). Eine
fallback-Route beantwortet Requests, die auf gar keine Route passten.
Einen Fehler werfen
Abschnitt betitelt „Einen Fehler werfen“Jeder Handler (oder jede Middleware) darf ein HttpError werfen; reject
ist die Kurzform:
import { HttpError, Status, reject } from 'actor-ts/http';
get(async (req) => { const user = await findUser(req.params.id); if (!user) reject(Status.NotFound, 'no such user', { id: req.params.id }); return completeJson(Status.OK, user);});Das Default-Mapping macht daraus 404 { "error": "no such user", "id": … }.
Ein HttpError kann auch Response-Header tragen (z. B. WWW-Authenticate):
throw new HttpError(Status.Unauthorized, 'login required', undefined, { 'www-authenticate': 'Bearer realm="api"',});Gescopte Behandlung — handleErrors
Abschnitt betitelt „Gescopte Behandlung — handleErrors“handleErrors(handler, child) fängt alles, was in child geworfen wird
(Handler und innere Middleware), und sieht den originalen Fehler —
vor jedem Default-Mapping. Gib eine Response zurück, um ihn zu behandeln,
oder null, um abzulehnen und einen äußeren Handler (oder den Default)
übernehmen zu lassen.
import { complete, completeJson, handleErrors, path, Status } from 'actor-ts/http';
const routes = handleErrors( (err) => err instanceof ValidationError ? completeJson(Status.BadRequest, { error: err.message, fields: err.fields }) : null, // ablehnen → eskalieren path('api', apiRoutes),);Handler verschachteln von außen nach innen wie withMiddleware: das
innerste handleErrors hat das erste Zugriffsrecht. Um eine
Auth-Middleware gelegt, fängt es auch deren Würfe (z. B. ein 401).
Unpassende Requests — fallback
Abschnitt betitelt „Unpassende Requests — fallback“fallback(handler) beantwortet jeden Request, der auf keine Route passte —
jede Methode, inklusive unpassender OPTIONS/HEAD. Es ist an den
Not-Found-Hook jedes Backends verdrahtet und muss daher an der Wurzel
des Routenbaums stehen (ein fallback unter path() wirft zur
Compile-Zeit); pro Server gibt es höchstens eines.
import { completeJson, concat, fallback, path, Status } from 'actor-ts/http';
const routes = concat( path('api', apiRoutes), fallback((req) => completeJson(Status.NotFound, { error: 'no such route', path: req.path })),);Serverweit — withErrorHandler
Abschnitt betitelt „Serverweit — withErrorHandler“Die letzte Instanz für Fehler, die jedem handleErrors entkommen, plus
backend-interne Fehler (Fehler beim Body-Parsen). Am Server-Builder setzen:
const binding = await system.http(8080) .withErrorHandler((err, req) => { system.log.error(`[http] ${req.method} ${req.path}`, err); return completeJson(Status.InternalServerError, { error: 'internal error' }); }) .bind(routes);Was der Client sieht — und was du behältst
Abschnitt betitelt „Was der Client sieht — und was du behältst“Der generische 500 aus Schritt 3 trägt { "error": "Internal Server Error" }
und sonst nichts — mit Absicht. Der geworfene Text enthält regelmäßig
Dateipfade, SQL-Fragmente, Connection-Strings oder einen Stack, und ein 500
ist per Definition ein Fall, für den niemand eine Meldung geschrieben hat.
Diese Redaktion funktioniert nur, weil das Detail serverseitig überlebt. Das
Framework loggt jeden Wurf, der handleErrors entkommt, auf error und
reicht den Fehlerwert durch, damit eine Senke, die Stacks formatiert,
weiterhin einen bekommt:
[http] POST /api/orders → 500 after 12 ms [x-request-id=01J8Z…]Ein geworfenes HttpError steht nicht in diesem Log — es ist eine Response,
die der Handler gewählt hat (ein 404, ein 401), und bleibt daher auf der
debug-Zeile pro Request, statt falschen Alarm zu schlagen. Dasselbe gilt
für einen Wurf in einer fallback-Route, der zu einem 500 gemappt und nicht
erneut geworfen wird: ohne die Logzeile bliebe davon nirgends eine Spur.
Der Zusatz [x-request-id=…] erscheint, wenn der Request diesen Header trug
und sein Wert eine wohlgeformte Id ist — siehe
Request-Id. Lies sie mit
requestIdOf(request) statt von Hand: die Funktion wendet dieselbe
Formprüfung an, und genau die verhindert, dass ein client-kontrollierter
String über einen eingebetteten Zeilenumbruch einen Log-Eintrag fälscht.
import { requestIdOf } from 'actor-ts/http';
const binding = await system.http(8080) .withErrorHandler((err, req) => { system.log.error(`[http] ${req.method} ${req.path} (${requestIdOf(req) ?? '-'})`, err); return completeJson(Status.InternalServerError, { error: 'internal error' }); }) .bind(routes);Hast du den Header über requestId({ headerName }) umbenannt, liest die
Zeile des Frameworks weiterhin den Default — logge deine Id dann aus
withErrorHandler.
Präzedenz
Abschnitt betitelt „Präzedenz“| Reihenfolge | Schicht | Sieht |
|---|---|---|
| 1 | innerstes → äußerstes handleErrors | den originalen Wurf; darf mit null ablehnen |
| 2 | withErrorHandler | was Schritt 1 entkam, plus backend-interne Fehler |
| 3 | Framework-Default | HttpError → Status + { error, …extra }; sonst generischer 500 |
Wirft ein Handler in Schritt 1 oder 2 selbst, übernimmt die nächste Schicht den neuen Fehler.
Wie weiter
Abschnitt betitelt „Wie weiter“- HTTP-Überblick — das große Ganze.
- HTML & XSS — sichere Fehlerseiten bauen.
- Sicherheit — der empfohlene Middleware-Stack.
