diff --git a/endpoint/exceptions.py b/endpoint/exceptions.py index f95139d7..0286b476 100644 --- a/endpoint/exceptions.py +++ b/endpoint/exceptions.py @@ -4,23 +4,32 @@ import json import werkzeug.exceptions -import werkzeug.wrappers +from odoo.http import Response -class RequestValidationError(werkzeug.exceptions.BadRequest): - """Bad request raised when the body fails JSON Schema validation. - Emits ``{"detail": [{"loc", "msg", "type"}, ...]}`` (FastAPI-style) - instead of the generic werkzeug HTML body. +class RequestValidationError(werkzeug.exceptions.HTTPException): + """Raised when the body fails JSON/XML schema validation. + + Emits ``{"detail": [{"loc", "msg", "type"}, ...]}`` (FastAPI-style) with a + 400 status instead of the generic werkzeug HTML body. + + ``code`` is left as ``None`` on purpose: this makes Odoo treat the + exception as a ready-made response and run the normal ``post_dispatch`` + hook on it, so route-managed headers (CORS, CSP, ...) are preserved. A real + ``code`` (e.g. via ``BadRequest``) would route through ``handle_error`` + instead and drop those headers. """ + code = None + def __init__(self, detail): super().__init__() self.detail = detail def get_response(self, environ=None, scope=None): - return werkzeug.wrappers.Response( + return Response( json.dumps({"detail": self.detail}), - status=self.code, + status=400, mimetype="application/json", ) diff --git a/endpoint/tests/test_endpoint_content_schema_validation.py b/endpoint/tests/test_endpoint_content_schema_validation.py index 711ba87b..c4abca8d 100644 --- a/endpoint/tests/test_endpoint_content_schema_validation.py +++ b/endpoint/tests/test_endpoint_content_schema_validation.py @@ -152,6 +152,21 @@ def test_json_invalid_body(self): self.assertEqual(payload["detail"][0]["loc"], ["body", "data"]) self.assertEqual(payload["detail"][0]["type"], "type") + @mute_logger("endpoint.endpoint") + def test_validation_error_preserves_cors_headers(self): + response = self.url_open( + "/demo/schema", + data=json.dumps({"data": "not-an-array"}), + headers={ + "Content-Type": "application/json", + "Origin": "https://editor.swagger.io", + }, + ) + self.assertEqual(response.status_code, 400) + # Route-managed CORS headers (default cors="*") must survive on the + # validation-error response, not only on a successful one. + self.assertEqual(response.headers.get("Access-Control-Allow-Origin"), "*") + @mute_logger("endpoint.endpoint") def test_json_malformed_body(self): response = self.url_open(