diff --git a/docs/error-masking.rst b/docs/error-masking.rst new file mode 100644 index 000000000..5c4f54a80 --- /dev/null +++ b/docs/error-masking.rst @@ -0,0 +1,49 @@ +Custom GraphQL Error Masking +============================ + +This project includes a custom error formatting function for GraphQL +responses that masks sensitive error details from clients. + +Purpose +------- + +- Prevent exposing internal error details for security and user experience. +- Allow whitelisting of exception classes that should be exposed as-is. +- Return a generic error message for all other exceptions. + +Configuration +------------- + +You can control the behavior using the ``GRAPHENE_ERRORS`` setting in your +Django settings file under the ``GRAPHENE`` namespace: + +.. code-block:: python + + GRAPHENE = { + "GRAPHENE_ERRORS": { + "MASK_EXCEPTIONS": True, # Enable or disable masking + "ERROR_MESSAGE": "A custom error message.", # Defaults to "Something went wrong. Please try again later." + "WHITELISTED_EXCEPTIONS": [ + "ValidationError", # Whitelist by class name + "django.core.exceptions.ValidationError", # Whitelist by full module path + "myapp.custom_exceptions.MyCustomException", # Custom exception whitelist by full path + ], + } + } + +Behavior +-------- + +- If ``MASK_EXCEPTIONS`` is False, all errors are returned fully formatted. +- If True, errors not in the whitelist will return only the generic message. +- Whitelisted exceptions are returned with full error details. + +Usage +----- + +The masking is automatically applied to the error responses of GraphQL +queries and mutations through a custom error formatter method. + +You can modify or extend the whitelisted exceptions as needed to suit your +project's error handling policy. + diff --git a/graphene_django/settings.py b/graphene_django/settings.py index da3370031..fb448c456 100644 --- a/graphene_django/settings.py +++ b/graphene_django/settings.py @@ -46,6 +46,7 @@ "ATOMIC_MUTATIONS": False, "TESTING_ENDPOINT": "/graphql", "MAX_VALIDATION_ERRORS": None, + "GRAPHENE_ERRORS": {}, } if settings.DEBUG: diff --git a/graphene_django/views.py b/graphene_django/views.py index 1ec659881..118270db9 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -228,9 +228,32 @@ def get_response(self, request, data, show_graphiql=False): if execution_result.errors: set_rollback() - response["errors"] = [ - self.format_error(e) for e in execution_result.errors - ] + + def safe_format(error): + config = getattr(graphene_settings, "GRAPHENE_ERRORS", {}) + mask_exceptions = config.get("MASK_EXCEPTIONS", False) + error_message= config.get("ERROR_MESSAGE", "Something went wrong. Please try again later.") + whitelist = config.get("WHITELISTED_EXCEPTIONS", []) + + if not mask_exceptions: + return self.format_error(error) + + original_error = getattr(error, "original_error", None) + if not original_error: + return {"message": error_message} + + error_class = type(original_error) + class_name = error_class.__name__ + full_path = f"{error_class.__module__}.{class_name}" + + if class_name in whitelist or full_path in whitelist: + return self.format_error(error) + + formatted = self.format_error(error) + formatted["message"] = error_message + return formatted + + response["errors"] = [safe_format(e) for e in execution_result.errors] if execution_result.errors and any( not getattr(e, "path", None) for e in execution_result.errors