Dart Shelf API with modules: auth (JWT + PBKDF2), tasks (CRUD + dopamine scorer), streaks (forgiveness + freeze), rewards (variable reward engine), time perception, sync (offline-first push/pull), rooms (body doubling placeholder). Includes DB migration (001_initial_schema.sql) and Docker Compose. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
71 lines
2.3 KiB
Dart
71 lines
2.3 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:logging/logging.dart';
|
|
import 'package:shelf/shelf.dart';
|
|
|
|
final _log = Logger('ErrorHandler');
|
|
|
|
// ── ApiException ──────────────────────────────────────────────────────
|
|
|
|
/// Typed exception that maps directly to an HTTP status code.
|
|
class ApiException implements Exception {
|
|
final int statusCode;
|
|
final String message;
|
|
final dynamic errors;
|
|
|
|
const ApiException(this.statusCode, this.message, {this.errors});
|
|
|
|
// Factory constructors for common HTTP errors
|
|
factory ApiException.badRequest(String message, {dynamic errors}) =>
|
|
ApiException(400, message, errors: errors);
|
|
|
|
factory ApiException.unauthorized([String message = 'Unauthorized']) =>
|
|
ApiException(401, message);
|
|
|
|
factory ApiException.forbidden([String message = 'Forbidden']) =>
|
|
ApiException(403, message);
|
|
|
|
factory ApiException.notFound([String message = 'Resource not found']) =>
|
|
ApiException(404, message);
|
|
|
|
factory ApiException.conflict([String message = 'Conflict']) =>
|
|
ApiException(409, message);
|
|
|
|
factory ApiException.tooManyRequests(
|
|
[String message = 'Too many requests']) =>
|
|
ApiException(429, message);
|
|
|
|
@override
|
|
String toString() => 'ApiException($statusCode): $message';
|
|
}
|
|
|
|
// ── Middleware ─────────────────────────────────────────────────────────
|
|
|
|
/// Catches all exceptions and returns a consistent JSON error response.
|
|
Middleware errorHandlerMiddleware() {
|
|
return (Handler innerHandler) {
|
|
return (Request request) async {
|
|
try {
|
|
return await innerHandler(request);
|
|
} on ApiException catch (e) {
|
|
return _jsonError(e.statusCode, e.message, errors: e.errors);
|
|
} catch (e, st) {
|
|
_log.severe('Unhandled exception', e, st);
|
|
return _jsonError(500, 'Internal server error');
|
|
}
|
|
};
|
|
};
|
|
}
|
|
|
|
Response _jsonError(int statusCode, String message, {dynamic errors}) {
|
|
return Response(
|
|
statusCode,
|
|
body: jsonEncode({
|
|
'success': false,
|
|
'message': message,
|
|
if (errors != null) 'errors': errors,
|
|
}),
|
|
headers: {'Content-Type': 'application/json'},
|
|
);
|
|
}
|