Files
cleanplate_api/lib/src/middleware/error_handler.dart
Oracle Public Cloud User 6bd1ab7e9f Initial commit: CleanPlate backend API
Dart Shelf REST API with auth, recipes, AI (Claude), search, and community modules.
PostgreSQL, Redis, Meilisearch. Docker Compose for local dev.
2026-03-04 14:52:13 +00:00

91 lines
3.0 KiB
Dart

import 'dart:convert';
import 'package:shelf/shelf.dart';
import 'package:logging/logging.dart';
final _log = Logger('ErrorHandler');
/// Global error-handling middleware.
///
/// Catches synchronous and asynchronous exceptions thrown by inner handlers
/// and converts them to structured JSON error responses.
Middleware errorHandler() {
return (Handler innerHandler) {
return (Request request) async {
try {
final response = await innerHandler(request);
return response;
} on ApiException catch (e) {
_log.warning('API error: ${e.code} - ${e.message}');
return Response(e.statusCode,
headers: {'content-type': 'application/json'},
body: jsonEncode({
'status': 'error',
'error': {
'code': e.code,
'message': e.message,
if (e.details != null) 'details': e.details,
},
}));
} on FormatException catch (e) {
_log.warning('Bad request: $e');
return Response(400,
headers: {'content-type': 'application/json'},
body: jsonEncode({
'status': 'error',
'error': {
'code': 'BAD_REQUEST',
'message': 'Invalid request format: ${e.message}',
},
}));
} catch (e, st) {
_log.severe('Unhandled exception', e, st);
return Response.internalServerError(
headers: {'content-type': 'application/json'},
body: jsonEncode({
'status': 'error',
'error': {
'code': 'INTERNAL_ERROR',
'message': 'Internal server error',
},
}),
);
}
};
};
}
/// A typed exception that the error handler can inspect to produce a proper
/// HTTP status and machine-readable error code.
class ApiException implements Exception {
final int statusCode;
final String code;
final String message;
final dynamic details;
ApiException(this.statusCode, this.code, this.message, {this.details});
factory ApiException.badRequest(String message, {dynamic details}) =>
ApiException(400, 'BAD_REQUEST', message, details: details);
factory ApiException.unauthorized([String message = 'Unauthorized']) =>
ApiException(401, 'UNAUTHORIZED', message);
factory ApiException.forbidden([String message = 'Forbidden']) =>
ApiException(403, 'FORBIDDEN', message);
factory ApiException.notFound([String message = 'Not found']) =>
ApiException(404, 'NOT_FOUND', message);
factory ApiException.conflict(String message) =>
ApiException(409, 'CONFLICT', message);
factory ApiException.tooManyRequests([String message = 'Too many requests']) =>
ApiException(429, 'TOO_MANY_REQUESTS', message);
factory ApiException.internal([String message = 'Internal server error']) =>
ApiException(500, 'INTERNAL_ERROR', message);
@override
String toString() => 'ApiException($statusCode, $code, $message)';
}