From 95ac0af13282b01cedaab59331adb24f129a575b Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 14 Jul 2025 06:49:11 +0100 Subject: [PATCH 1/8] fix(api): add cors headers to error responses The errorHandler middleware was not adding CORS headers to the error responses it generated. This caused browsers to block client-side applications from reading the response body due to the Same-Origin Policy, resulting in generic network errors instead of specific API error messages. This change refactors the errorHandler to use a helper function that constructs the JSON error response while also adding the necessary `Access-Control-Allow-Origin` header. This ensures that all error responses are readable by the client, fixing the bug. --- lib/src/middlewares/error_handler.dart | 78 +++++++++++++++++--------- 1 file changed, 50 insertions(+), 28 deletions(-) diff --git a/lib/src/middlewares/error_handler.dart b/lib/src/middlewares/error_handler.dart index a51fcee..20010d1 100644 --- a/lib/src/middlewares/error_handler.dart +++ b/lib/src/middlewares/error_handler.dart @@ -19,13 +19,11 @@ 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. @@ -33,40 +31,26 @@ Middleware errorHandler() { 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, ); } }; @@ -108,3 +92,41 @@ 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 = { + HttpHeaders.contentTypeHeader: 'application/json', + }; + + // Add CORS headers to error responses to allow the client to read them. + // This logic mirrors the behavior of `shelf_cors_headers` for development. + final origin = context.request.headers['Origin']; + if (origin != null) { + // A simple check for localhost development environments. + // For production, this should be a more robust check against a list + // of allowed origins from environment variables. + if (Uri.tryParse(origin)?.host == 'localhost') { + headers[HttpHeaders.accessControlAllowOriginHeader] = origin; + 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, + ); +} From 156b6ff2d4ecca94814345c9ffa1132c95c08edc Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 14 Jul 2025 06:53:48 +0100 Subject: [PATCH 2/8] fix(api): implement production-ready cors in error handler The errorHandler middleware was not adding CORS headers to error responses in a production-ready way. This caused browsers to block client-side applications from reading the response body, resulting in generic network errors instead of specific API error messages. This change refactors the errorHandler to use an environment-aware helper function. It now checks for a `CORS_ALLOWED_ORIGIN` environment variable for production and falls back to allowing any `localhost` origin for development. This aligns the error response behavior with the main CORS middleware and the project's documentation, fixing the bug. --- lib/src/config/environment_config.dart | 7 +++++++ lib/src/middlewares/error_handler.dart | 28 +++++++++++++++++--------- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/lib/src/config/environment_config.dart b/lib/src/config/environment_config.dart index fd8e18a..0b0dc25 100644 --- a/lib/src/config/environment_config.dart +++ b/lib/src/config/environment_config.dart @@ -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']; } diff --git a/lib/src/middlewares/error_handler.dart b/lib/src/middlewares/error_handler.dart index 20010d1..2bf1162 100644 --- a/lib/src/middlewares/error_handler.dart +++ b/lib/src/middlewares/error_handler.dart @@ -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'; @@ -108,15 +109,24 @@ Response _jsonErrorResponse({ HttpHeaders.contentTypeHeader: 'application/json', }; - // Add CORS headers to error responses to allow the client to read them. - // This logic mirrors the behavior of `shelf_cors_headers` for development. - final origin = context.request.headers['Origin']; - if (origin != null) { - // A simple check for localhost development environments. - // For production, this should be a more robust check against a list - // of allowed origins from environment variables. - if (Uri.tryParse(origin)?.host == 'localhost') { - headers[HttpHeaders.accessControlAllowOriginHeader] = origin; + // 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.accessControlAllowMethodsHeader] = 'GET, POST, PUT, DELETE, OPTIONS'; headers[HttpHeaders.accessControlAllowHeadersHeader] = From f03fe954437734be4ec5f80abce4ef63344db1ad Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 14 Jul 2025 06:58:25 +0100 Subject: [PATCH 3/8] docs(api): add CORS_ALLOWED_ORIGIN to .env.example Adds the `CORS_ALLOWED_ORIGIN` environment variable to the example configuration file. This synchronizes the `.env.example` with the existing documentation and recent CORS-related code changes, ensuring developers are aware of this required setting for production deployments. --- .env.example | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 1f4422a..6f07c86 100644 --- a/.env.example +++ b/.env.example @@ -2,4 +2,9 @@ # 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" \ No newline at end of file +DATABASE_URL="mongodb://user:password@localhost:27017/ht_api_db" + +# (Optional for Production) The specific origin URL of your web client (e.g., the HT Dashboard). +# This is required for production deployments to allow cross-origin requests. +# For local development, this can be left unset as 'localhost' is allowed by default. +# CORS_ALLOWED_ORIGIN="https://your-dashboard.com" \ No newline at end of file From c21990340d0ee80c85e2e93544803afe423a0316 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 14 Jul 2025 06:58:30 +0100 Subject: [PATCH 4/8] docs(api): add CORS_ALLOWED_ORIGIN to .env.example Adds the `CORS_ALLOWED_ORIGIN` environment variable to the example configuration file. This synchronizes the `.env.example` with the existing documentation and recent CORS-related code changes, ensuring developers are aware of this required setting for production deployments. --- .env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 6f07c86..7f14444 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,7 @@ # 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" +# DATABASE_URL="mongodb://user:password@localhost:27017/ht_api_db" # (Optional for Production) The specific origin URL of your web client (e.g., the HT Dashboard). # This is required for production deployments to allow cross-origin requests. From 1a58ecdf2dcfd54f9df02ce0c4d79a923aafc93a Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 14 Jul 2025 06:58:34 +0100 Subject: [PATCH 5/8] docs(api): align README environment example with .env.example Updates the environment variable example in the README.md to match the format and content of `.env.example`. This change adds comments explaining each variable and comments them out by default, improving clarity and ensuring consistency across project documentation. --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 93e9aae..6ec729d 100644 --- a/README.md +++ b/README.md @@ -80,8 +80,14 @@ for more details. 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" + ```shell + # The full connection string for your MongoDB instance. + # Required for the application to connect to the database. + # DATABASE_URL="mongodb://user:password@localhost:27017/ht_api_db" + + # (Optional for Production) The specific origin URL of your web client. + # This is required for production deployments to allow cross-origin requests. + # CORS_ALLOWED_ORIGIN="https://your-dashboard.com" ``` 3. **Clone the repository:** From 5f43537cdb2e4590775ea64cab031baf6890999b Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 14 Jul 2025 07:00:22 +0100 Subject: [PATCH 6/8] docs(api): improve clarity of .env.example comments Updates the comments in the `.env.example` file to be more explicit and unambiguous. The new comments clearly state when each variable is required (e.g., "REQUIRED" vs. "REQUIRED FOR PRODUCTION") and provide better context for their purpose. This resolves confusion around the previous "Optional for Production" wording. --- .env.example | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 7f14444..86ad6eb 100644 --- a/.env.example +++ b/.env.example @@ -2,9 +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. +# 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" -# (Optional for Production) The specific origin URL of your web client (e.g., the HT Dashboard). -# This is required for production deployments to allow cross-origin requests. +# 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" \ No newline at end of file From 59297c55f81768e057c8a58dec5f19b9872a19fc Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 14 Jul 2025 07:06:55 +0100 Subject: [PATCH 7/8] fix(api): add credentials header to cors error responses The errorHandler middleware was missing the `Access-Control-Allow-Credentials` header in its CORS configuration. This caused browsers to block credentialed requests (e.g., those with an Authorization header) that resulted in an error, leading to a specific CORS failure. This change adds the `Access-Control-Allow-Credentials: true` header to all error responses when the origin is allowed, resolving the issue and allowing the client to correctly read API error messages for authenticated or credentialed requests. --- lib/src/middlewares/error_handler.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/middlewares/error_handler.dart b/lib/src/middlewares/error_handler.dart index 2bf1162..e51d0ad 100644 --- a/lib/src/middlewares/error_handler.dart +++ b/lib/src/middlewares/error_handler.dart @@ -127,6 +127,7 @@ Response _jsonErrorResponse({ if (isOriginAllowed) { headers[HttpHeaders.accessControlAllowOriginHeader] = requestOrigin; + headers[HttpHeaders.accessControlAllowCredentialsHeader] = 'true'; headers[HttpHeaders.accessControlAllowMethodsHeader] = 'GET, POST, PUT, DELETE, OPTIONS'; headers[HttpHeaders.accessControlAllowHeadersHeader] = From fdc87c821e25aba6b0a1d5adf838f11023c2895c Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 14 Jul 2025 07:10:18 +0100 Subject: [PATCH 8/8] docs(api): simplify and clarify environment setup in README Updates the "Configuration" section in the README.md to be clearer and more direct. Instead of instructing users to create a .env file from scratch and duplicating the example content, it now instructs them to copy the existing `.env.example` file. This removes redundancy and improves the developer setup experience. --- README.md | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 6ec729d..c51f7be 100644 --- a/README.md +++ b/README.md @@ -75,20 +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. - - Create a `.env` file in the root of the project or export the variable in - your shell: - ```shell - # The full connection string for your MongoDB instance. - # Required for the application to connect to the database. - # DATABASE_URL="mongodb://user:password@localhost:27017/ht_api_db" - - # (Optional for Production) The specific origin URL of your web client. - # This is required for production deployments to allow cross-origin requests. - # CORS_ALLOWED_ORIGIN="https://your-dashboard.com" + Before running the server, you must configure the necessary environment + variables. + + 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