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.
This commit is contained in:
90
lib/src/middleware/error_handler.dart
Normal file
90
lib/src/middleware/error_handler.dart
Normal file
@@ -0,0 +1,90 @@
|
||||
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)';
|
||||
}
|
||||
Reference in New Issue
Block a user