Dart Shelf REST API with auth, recipes, AI (Claude), search, and community modules. PostgreSQL, Redis, Meilisearch. Docker Compose for local dev.
91 lines
3.0 KiB
Dart
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)';
|
|
}
|