Skip to content

Fix un propagated exception messages #18

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jul 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,11 @@
# Copy this file to .env and fill in your actual configuration values.
# The .env file is ignored by Git and should NOT be committed.

DATABASE_URL="mongodb://user:password@localhost:27017/ht_api_db"
# REQUIRED: The full connection string for your MongoDB instance.
# The application cannot start without a database connection.
# DATABASE_URL="mongodb://user:password@localhost:27017/ht_api_db"

# REQUIRED FOR PRODUCTION: The specific origin URL of your web client.
# This allows the client (e.g., the HT Dashboard) to make requests to the API.
# For local development, this can be left unset as 'localhost' is allowed by default.
# CORS_ALLOWED_ORIGIN="https://your-dashboard.com"
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,15 @@ for more details.
* Dart Frog CLI (`dart pub global activate dart_frog_cli`)

2. **Configuration:**
Before running the server, you must configure the database connection by
setting the `DATABASE_URL` environment variable.
Before running the server, you must configure the necessary environment
variables.

Create a `.env` file in the root of the project or export the variable in
your shell:
```
DATABASE_URL="mongodb://user:password@localhost:27017/ht_api_db"
Copy the `.env.example` file to a new file named `.env`:
```bash
cp .env.example .env
```
Then, open the new `.env` file and update the variables with your actual
configuration values, such as the `DATABASE_URL`.

3. **Clone the repository:**
```bash
Expand Down
7 changes: 7 additions & 0 deletions lib/src/config/environment_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,11 @@ abstract final class EnvironmentConfig {
/// The value is read from the `ENV` environment variable.
/// Defaults to 'production' if the variable is not set.
static String get environment => _env['ENV'] ?? 'production';

/// Retrieves the allowed CORS origin from the environment.
///
/// The value is read from the `CORS_ALLOWED_ORIGIN` environment variable.
/// This is used to configure CORS for production environments.
/// Returns `null` if the variable is not set.
static String? get corsAllowedOrigin => _env['CORS_ALLOWED_ORIGIN'];
}
89 changes: 61 additions & 28 deletions lib/src/middlewares/error_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import 'dart:io';

import 'package:dart_frog/dart_frog.dart';
import 'package:ht_api/src/config/environment_config.dart';
import 'package:ht_shared/ht_shared.dart';
import 'package:json_annotation/json_annotation.dart';

Expand All @@ -19,54 +20,38 @@ Middleware errorHandler() {
} on HtHttpException catch (e, stackTrace) {
// Handle specific HtHttpExceptions from the client/repository layers
final statusCode = _mapExceptionToStatusCode(e);
final errorCode = _mapExceptionToCodeString(e);
print('HtHttpException Caught: $e\n$stackTrace'); // Log for debugging
return Response.json(
return _jsonErrorResponse(
statusCode: statusCode,
body: {
'error': {'code': errorCode, 'message': e.message},
},
exception: e,
context: context,
);
} on CheckedFromJsonException catch (e, stackTrace) {
// Handle json_serializable validation errors. These are client errors.
final field = e.key ?? 'unknown';
final message = 'Invalid request body: Field "$field" has an '
'invalid value or is missing. ${e.message}';
print('CheckedFromJsonException Caught: $e\n$stackTrace');
return Response.json(
return _jsonErrorResponse(
statusCode: HttpStatus.badRequest, // 400
body: {
'error': {
'code': 'invalidField',
'message': message,
},
},
exception: InvalidInputException(message),
context: context,
);
} on FormatException catch (e, stackTrace) {
// Handle data format/parsing errors (often indicates bad client input)
print('FormatException Caught: $e\n$stackTrace'); // Log for debugging
return Response.json(
return _jsonErrorResponse(
statusCode: HttpStatus.badRequest, // 400
body: {
'error': {
'code': 'invalidFormat',
'message': 'Invalid data format: ${e.message}',
},
},
exception: InvalidInputException('Invalid data format: ${e.message}'),
context: context,
);
} catch (e, stackTrace) {
// Handle any other unexpected errors
print('Unhandled Exception Caught: $e\n$stackTrace');
return Response.json(
return _jsonErrorResponse(
statusCode: HttpStatus.internalServerError, // 500
body: {
'error': {
'code': 'internalServerError',
'message': 'An unexpected internal server error occurred.',
// Avoid leaking sensitive details in production responses
// 'details': e.toString(), // Maybe include in dev mode only
},
},
exception: const UnknownException('An unexpected internal server error occurred.'),
context: context,
);
}
};
Expand Down Expand Up @@ -108,3 +93,51 @@ String _mapExceptionToCodeString(HtHttpException exception) {
_ => 'unknownError', // Default
};
}

/// Creates a standardized JSON error response with appropriate CORS headers.
///
/// This helper ensures that error responses sent to the client include the
/// necessary `Access-Control-Allow-Origin` header, allowing the client-side
/// application to read the error message body.
Response _jsonErrorResponse({
required int statusCode,
required HtHttpException exception,
required RequestContext context,
}) {
final errorCode = _mapExceptionToCodeString(exception);
final headers = <String, String>{
HttpHeaders.contentTypeHeader: 'application/json',
};

// Add CORS headers to error responses. This logic is environment-aware.
// In production, it uses a specific origin from `CORS_ALLOWED_ORIGIN`.
// In development (if the variable is not set), it allows any localhost.
final requestOrigin = context.request.headers['Origin'];
if (requestOrigin != null) {
final allowedOrigin = EnvironmentConfig.corsAllowedOrigin;

var isOriginAllowed = false;
if (allowedOrigin != null) {
// Production: Check against the specific allowed origin.
isOriginAllowed = (requestOrigin == allowedOrigin);
} else {
// Development: Allow any localhost origin.
isOriginAllowed = (Uri.tryParse(requestOrigin)?.host == 'localhost');
}

if (isOriginAllowed) {
headers[HttpHeaders.accessControlAllowOriginHeader] = requestOrigin;
headers[HttpHeaders.accessControlAllowCredentialsHeader] = 'true';
headers[HttpHeaders.accessControlAllowMethodsHeader] =
'GET, POST, PUT, DELETE, OPTIONS';
headers[HttpHeaders.accessControlAllowHeadersHeader] =
'Origin, Content-Type, Authorization';
}
}

return Response.json(
statusCode: statusCode,
body: {'error': {'code': errorCode, 'message': exception.message}},
headers: headers,
);
}
Loading