- Client: API-Basis-URL (joinApiUrl, /v1-Falle), Vite strictPort + Proxy 127.0.0.1, Nicht-JSON-Fehler - Server: /api-404 ohne Wildcard-Bug, SPA-Fallback, Auth-Middleware, Cron, Mailer, Crypto - Routen: OAuth-State, Email/Stripe/Analytics; client/.env.example Made-with: Cursor
126 lines
3.1 KiB
JavaScript
126 lines
3.1 KiB
JavaScript
/**
|
|
* Global Error Handler Middleware
|
|
* Catches all errors and returns consistent JSON responses
|
|
*/
|
|
|
|
import { AppwriteException } from 'node-appwrite'
|
|
|
|
export class AppError extends Error {
|
|
constructor(message, statusCode = 500, code = 'INTERNAL_ERROR') {
|
|
super(message)
|
|
this.statusCode = statusCode
|
|
this.code = code
|
|
this.isOperational = true
|
|
Error.captureStackTrace(this, this.constructor)
|
|
}
|
|
}
|
|
|
|
export class ValidationError extends AppError {
|
|
constructor(message, fields = {}) {
|
|
super(message, 400, 'VALIDATION_ERROR')
|
|
this.fields = fields
|
|
}
|
|
}
|
|
|
|
export class AuthenticationError extends AppError {
|
|
constructor(message = 'Nicht authentifiziert') {
|
|
super(message, 401, 'AUTHENTICATION_ERROR')
|
|
}
|
|
}
|
|
|
|
export class AuthorizationError extends AppError {
|
|
constructor(message = 'Keine Berechtigung') {
|
|
super(message, 403, 'AUTHORIZATION_ERROR')
|
|
}
|
|
}
|
|
|
|
export class NotFoundError extends AppError {
|
|
constructor(resource = 'Ressource') {
|
|
super(`${resource} nicht gefunden`, 404, 'NOT_FOUND')
|
|
}
|
|
}
|
|
|
|
export class RateLimitError extends AppError {
|
|
constructor(message = 'Zu viele Anfragen') {
|
|
super(message, 429, 'RATE_LIMIT_EXCEEDED')
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Error handler middleware
|
|
*/
|
|
export function errorHandler(err, req, res, next) {
|
|
// Log error
|
|
console.error(`[ERROR] ${new Date().toISOString()}`, {
|
|
method: req.method,
|
|
path: req.path,
|
|
error: err.message,
|
|
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined,
|
|
})
|
|
|
|
// Default error values (AppwriteException uses numeric err.code — do not reuse as JSON "code" string)
|
|
let statusCode =
|
|
typeof err.statusCode === 'number' ? err.statusCode : undefined
|
|
let code = typeof err.code === 'string' && err.code ? err.code : 'INTERNAL_ERROR'
|
|
let message = err.message || 'Ein Fehler ist aufgetreten'
|
|
|
|
if (
|
|
err instanceof AppwriteException &&
|
|
typeof err.code === 'number' &&
|
|
err.code >= 400 &&
|
|
err.code < 600
|
|
) {
|
|
statusCode = err.code
|
|
code = err.type || 'APPWRITE_ERROR'
|
|
message = err.message || message
|
|
err.isOperational = true
|
|
}
|
|
|
|
if (statusCode === undefined) {
|
|
statusCode = 500
|
|
}
|
|
|
|
// Handle specific error types
|
|
if (err.name === 'ValidationError') {
|
|
statusCode = 400
|
|
code = 'VALIDATION_ERROR'
|
|
}
|
|
|
|
if (err.name === 'JsonWebTokenError') {
|
|
statusCode = 401
|
|
code = 'INVALID_TOKEN'
|
|
message = 'Ungültiger Token'
|
|
}
|
|
|
|
if (err.name === 'TokenExpiredError') {
|
|
statusCode = 401
|
|
code = 'TOKEN_EXPIRED'
|
|
message = 'Token abgelaufen'
|
|
}
|
|
|
|
// Don't expose internal errors in production
|
|
if (!err.isOperational && process.env.NODE_ENV === 'production') {
|
|
message = 'Ein interner Fehler ist aufgetreten'
|
|
}
|
|
|
|
// Send response
|
|
res.status(statusCode).json({
|
|
success: false,
|
|
error: {
|
|
code,
|
|
message,
|
|
...(err.fields && { fields: err.fields }),
|
|
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
|
|
},
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Async handler wrapper to catch errors in async routes
|
|
*/
|
|
export function asyncHandler(fn) {
|
|
return (req, res, next) => {
|
|
Promise.resolve(fn(req, res, next)).catch(next)
|
|
}
|
|
}
|