Skip to content

Commit d532bc4

Browse files
authored
Merge pull request #18 from headlines-toolkit/fix_un_propagated_exception_messages
Fix un propagated exception messages
2 parents 46bba64 + fdc87c8 commit d532bc4

File tree

4 files changed

+83
-35
lines changed

4 files changed

+83
-35
lines changed

.env.example

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,11 @@
22
# Copy this file to .env and fill in your actual configuration values.
33
# The .env file is ignored by Git and should NOT be committed.
44

5-
DATABASE_URL="mongodb://user:password@localhost:27017/ht_api_db"
5+
# REQUIRED: The full connection string for your MongoDB instance.
6+
# The application cannot start without a database connection.
7+
# DATABASE_URL="mongodb://user:password@localhost:27017/ht_api_db"
8+
9+
# REQUIRED FOR PRODUCTION: The specific origin URL of your web client.
10+
# This allows the client (e.g., the HT Dashboard) to make requests to the API.
11+
# For local development, this can be left unset as 'localhost' is allowed by default.
12+
# CORS_ALLOWED_ORIGIN="https://your-dashboard.com"

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,15 @@ for more details.
7575
* Dart Frog CLI (`dart pub global activate dart_frog_cli`)
7676

7777
2. **Configuration:**
78-
Before running the server, you must configure the database connection by
79-
setting the `DATABASE_URL` environment variable.
78+
Before running the server, you must configure the necessary environment
79+
variables.
8080

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

8788
3. **Clone the repository:**
8889
```bash

lib/src/config/environment_config.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,11 @@ abstract final class EnvironmentConfig {
7979
/// The value is read from the `ENV` environment variable.
8080
/// Defaults to 'production' if the variable is not set.
8181
static String get environment => _env['ENV'] ?? 'production';
82+
83+
/// Retrieves the allowed CORS origin from the environment.
84+
///
85+
/// The value is read from the `CORS_ALLOWED_ORIGIN` environment variable.
86+
/// This is used to configure CORS for production environments.
87+
/// Returns `null` if the variable is not set.
88+
static String? get corsAllowedOrigin => _env['CORS_ALLOWED_ORIGIN'];
8289
}

lib/src/middlewares/error_handler.dart

Lines changed: 61 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import 'dart:io';
55

66
import 'package:dart_frog/dart_frog.dart';
7+
import 'package:ht_api/src/config/environment_config.dart';
78
import 'package:ht_shared/ht_shared.dart';
89
import 'package:json_annotation/json_annotation.dart';
910

@@ -19,54 +20,38 @@ Middleware errorHandler() {
1920
} on HtHttpException catch (e, stackTrace) {
2021
// Handle specific HtHttpExceptions from the client/repository layers
2122
final statusCode = _mapExceptionToStatusCode(e);
22-
final errorCode = _mapExceptionToCodeString(e);
2323
print('HtHttpException Caught: $e\n$stackTrace'); // Log for debugging
24-
return Response.json(
24+
return _jsonErrorResponse(
2525
statusCode: statusCode,
26-
body: {
27-
'error': {'code': errorCode, 'message': e.message},
28-
},
26+
exception: e,
27+
context: context,
2928
);
3029
} on CheckedFromJsonException catch (e, stackTrace) {
3130
// Handle json_serializable validation errors. These are client errors.
3231
final field = e.key ?? 'unknown';
3332
final message = 'Invalid request body: Field "$field" has an '
3433
'invalid value or is missing. ${e.message}';
3534
print('CheckedFromJsonException Caught: $e\n$stackTrace');
36-
return Response.json(
35+
return _jsonErrorResponse(
3736
statusCode: HttpStatus.badRequest, // 400
38-
body: {
39-
'error': {
40-
'code': 'invalidField',
41-
'message': message,
42-
},
43-
},
37+
exception: InvalidInputException(message),
38+
context: context,
4439
);
4540
} on FormatException catch (e, stackTrace) {
4641
// Handle data format/parsing errors (often indicates bad client input)
4742
print('FormatException Caught: $e\n$stackTrace'); // Log for debugging
48-
return Response.json(
43+
return _jsonErrorResponse(
4944
statusCode: HttpStatus.badRequest, // 400
50-
body: {
51-
'error': {
52-
'code': 'invalidFormat',
53-
'message': 'Invalid data format: ${e.message}',
54-
},
55-
},
45+
exception: InvalidInputException('Invalid data format: ${e.message}'),
46+
context: context,
5647
);
5748
} catch (e, stackTrace) {
5849
// Handle any other unexpected errors
5950
print('Unhandled Exception Caught: $e\n$stackTrace');
60-
return Response.json(
51+
return _jsonErrorResponse(
6152
statusCode: HttpStatus.internalServerError, // 500
62-
body: {
63-
'error': {
64-
'code': 'internalServerError',
65-
'message': 'An unexpected internal server error occurred.',
66-
// Avoid leaking sensitive details in production responses
67-
// 'details': e.toString(), // Maybe include in dev mode only
68-
},
69-
},
53+
exception: const UnknownException('An unexpected internal server error occurred.'),
54+
context: context,
7055
);
7156
}
7257
};
@@ -108,3 +93,51 @@ String _mapExceptionToCodeString(HtHttpException exception) {
10893
_ => 'unknownError', // Default
10994
};
11095
}
96+
97+
/// Creates a standardized JSON error response with appropriate CORS headers.
98+
///
99+
/// This helper ensures that error responses sent to the client include the
100+
/// necessary `Access-Control-Allow-Origin` header, allowing the client-side
101+
/// application to read the error message body.
102+
Response _jsonErrorResponse({
103+
required int statusCode,
104+
required HtHttpException exception,
105+
required RequestContext context,
106+
}) {
107+
final errorCode = _mapExceptionToCodeString(exception);
108+
final headers = <String, String>{
109+
HttpHeaders.contentTypeHeader: 'application/json',
110+
};
111+
112+
// Add CORS headers to error responses. This logic is environment-aware.
113+
// In production, it uses a specific origin from `CORS_ALLOWED_ORIGIN`.
114+
// In development (if the variable is not set), it allows any localhost.
115+
final requestOrigin = context.request.headers['Origin'];
116+
if (requestOrigin != null) {
117+
final allowedOrigin = EnvironmentConfig.corsAllowedOrigin;
118+
119+
var isOriginAllowed = false;
120+
if (allowedOrigin != null) {
121+
// Production: Check against the specific allowed origin.
122+
isOriginAllowed = (requestOrigin == allowedOrigin);
123+
} else {
124+
// Development: Allow any localhost origin.
125+
isOriginAllowed = (Uri.tryParse(requestOrigin)?.host == 'localhost');
126+
}
127+
128+
if (isOriginAllowed) {
129+
headers[HttpHeaders.accessControlAllowOriginHeader] = requestOrigin;
130+
headers[HttpHeaders.accessControlAllowCredentialsHeader] = 'true';
131+
headers[HttpHeaders.accessControlAllowMethodsHeader] =
132+
'GET, POST, PUT, DELETE, OPTIONS';
133+
headers[HttpHeaders.accessControlAllowHeadersHeader] =
134+
'Origin, Content-Type, Authorization';
135+
}
136+
}
137+
138+
return Response.json(
139+
statusCode: statusCode,
140+
body: {'error': {'code': errorCode, 'message': exception.message}},
141+
headers: headers,
142+
);
143+
}

0 commit comments

Comments
 (0)