1fanwang commented on code in PR #66888:
URL: https://github.com/apache/airflow/pull/66888#discussion_r3286594337


##########
airflow-core/src/airflow/api_fastapi/common/exceptions.py:
##########
@@ -108,6 +108,46 @@ def _is_dialect_matched(self, exc: IntegrityError) -> bool:
         return False
 
 
+class _DataErrorHandler(BaseErrorHandler[DataError]):
+    """
+    Translate ``sqlalchemy.exc.DataError`` into an actionable HTTP response.
+
+    ``DataError`` wraps the database rejecting an INSERT/UPDATE because a value
+    exceeds the column's declared type (MySQL ``1406 Data too long``, Postgres
+    ``value too long for type``) or is out of range (MySQL ``1264 Out of range
+    value``, Postgres ``numeric field overflow``). The wrapped value came from
+    request input that passed Pydantic validation, so the failure is always a
+    client problem — translate to a 4xx with an actionable hint rather than
+    surfacing as a generic 500.
+    """
+
+    _TOO_LARGE_MARKERS: tuple[str, ...] = ("too long", "too large", "too big")
+
+    def __init__(self):
+        super().__init__(DataError)
+
+    def exception_handler(self, request: Request, exc: DataError):
+        orig_error = str(exc.orig)
+        if any(marker in orig_error.lower() for marker in 
self._TOO_LARGE_MARKERS):
+            status_code = status.HTTP_413_CONTENT_TOO_LARGE
+            reason = "Payload exceeded database column limit"
+        else:
+            status_code = status.HTTP_422_UNPROCESSABLE_ENTITY
+            reason = "Value rejected by database"
+        raise HTTPException(
+            status_code=status_code,
+            detail={
+                "reason": reason,
+                "orig_error": orig_error,
+                "message": (
+                    "Database rejected the payload. Reduce the field size, or "
+                    "your operator may widen the column type (e.g. MEDIUMTEXT 
/ "
+                    "LONGTEXT on MySQL)."
+                ),
+            },

Review Comment:
   Done in 3f8fdc6a20 — added `statement` to the response detail to mirror the 
unique-constraint shape.



##########
airflow-core/tests/unit/api_fastapi/common/test_exceptions.py:
##########
@@ -388,6 +391,86 @@ def 
test_handle_multiple_columns_unique_constraint_error_with_stacktrace(
         assert exeinfo_response_error.value.detail == expected_exception.detail
 
 
+class TestDataErrorHandler:
+    handler = _DataErrorHandler()
+
+    @staticmethod
+    def _make_data_error(orig_msg: str) -> DataError:
+        return DataError(
+            statement="INSERT INTO dag_run (conf) VALUES (?)",
+            params={},
+            orig=Exception(orig_msg),
+        )
+
+    @pytest.mark.parametrize(
+        ("orig_msg", "expected_status", "expected_reason"),
+        [
+            pytest.param(
+                "(1406, \"Data too long for column 'conf' at row 1\")",
+                status.HTTP_413_CONTENT_TOO_LARGE,
+                "Payload exceeded database column limit",
+                id="mysql-1406-data-too-long",
+            ),
+            pytest.param(
+                "value too long for type character varying(250)",
+                status.HTTP_413_CONTENT_TOO_LARGE,
+                "Payload exceeded database column limit",
+                id="postgres-value-too-long",
+            ),
+            pytest.param(
+                "string or blob too big",
+                status.HTTP_413_CONTENT_TOO_LARGE,
+                "Payload exceeded database column limit",
+                id="sqlite-blob-too-big",
+            ),
+            pytest.param(
+                "(1264, \"Out of range value for column 'slots' at row 1\")",
+                status.HTTP_422_UNPROCESSABLE_ENTITY,
+                "Value rejected by database",
+                id="mysql-1264-out-of-range",
+            ),
+            pytest.param(
+                "numeric field overflow",
+                status.HTTP_422_UNPROCESSABLE_ENTITY,
+                "Value rejected by database",
+                id="postgres-numeric-field-overflow",
+            ),
+        ],
+    )
+    def test_dataerror_translates_to_actionable_http_response(
+        self,
+        orig_msg: str,
+        expected_status: int,
+        expected_reason: str,
+    ) -> None:
+        exc = self._make_data_error(orig_msg)
+        with pytest.raises(HTTPException) as exc_info:
+            self.handler.exception_handler(Mock(), exc)
+        assert exc_info.value.status_code == expected_status
+        detail = exc_info.value.detail
+        assert isinstance(detail, dict)
+        assert detail["reason"] == expected_reason
+        assert detail["orig_error"] == orig_msg
+        assert "MEDIUMTEXT" in detail["message"]
+
+    def test_dataerror_dispatched_through_fastapi_app(self) -> None:

Review Comment:
   Done in 3f8fdc6a20.



##########
airflow-core/tests/unit/api_fastapi/common/test_exceptions.py:
##########
@@ -388,6 +391,86 @@ def 
test_handle_multiple_columns_unique_constraint_error_with_stacktrace(
         assert exeinfo_response_error.value.detail == expected_exception.detail
 
 
+class TestDataErrorHandler:
+    handler = _DataErrorHandler()
+
+    @staticmethod
+    def _make_data_error(orig_msg: str) -> DataError:
+        return DataError(
+            statement="INSERT INTO dag_run (conf) VALUES (?)",
+            params={},
+            orig=Exception(orig_msg),
+        )
+
+    @pytest.mark.parametrize(
+        ("orig_msg", "expected_status", "expected_reason"),
+        [
+            pytest.param(
+                "(1406, \"Data too long for column 'conf' at row 1\")",
+                status.HTTP_413_CONTENT_TOO_LARGE,
+                "Payload exceeded database column limit",
+                id="mysql-1406-data-too-long",
+            ),
+            pytest.param(
+                "value too long for type character varying(250)",
+                status.HTTP_413_CONTENT_TOO_LARGE,
+                "Payload exceeded database column limit",
+                id="postgres-value-too-long",
+            ),
+            pytest.param(
+                "string or blob too big",
+                status.HTTP_413_CONTENT_TOO_LARGE,
+                "Payload exceeded database column limit",
+                id="sqlite-blob-too-big",
+            ),
+            pytest.param(
+                "(1264, \"Out of range value for column 'slots' at row 1\")",
+                status.HTTP_422_UNPROCESSABLE_ENTITY,
+                "Value rejected by database",
+                id="mysql-1264-out-of-range",
+            ),
+            pytest.param(
+                "numeric field overflow",
+                status.HTTP_422_UNPROCESSABLE_ENTITY,
+                "Value rejected by database",
+                id="postgres-numeric-field-overflow",
+            ),
+        ],
+    )
+    def test_dataerror_translates_to_actionable_http_response(

Review Comment:
   Done in 3f8fdc6a20.



##########
airflow-core/src/airflow/api_fastapi/common/exceptions.py:
##########
@@ -108,6 +108,46 @@ def _is_dialect_matched(self, exc: IntegrityError) -> bool:
         return False
 
 
+class _DataErrorHandler(BaseErrorHandler[DataError]):
+    """
+    Translate ``sqlalchemy.exc.DataError`` into an actionable HTTP response.
+
+    ``DataError`` wraps the database rejecting an INSERT/UPDATE because a value
+    exceeds the column's declared type (MySQL ``1406 Data too long``, Postgres
+    ``value too long for type``) or is out of range (MySQL ``1264 Out of range
+    value``, Postgres ``numeric field overflow``). The wrapped value came from
+    request input that passed Pydantic validation, so the failure is always a
+    client problem — translate to a 4xx with an actionable hint rather than
+    surfacing as a generic 500.
+    """
+
+    _TOO_LARGE_MARKERS: tuple[str, ...] = ("too long", "too large", "too big")
+
+    def __init__(self):
+        super().__init__(DataError)
+
+    def exception_handler(self, request: Request, exc: DataError):
+        orig_error = str(exc.orig)
+        if any(marker in orig_error.lower() for marker in 
self._TOO_LARGE_MARKERS):
+            status_code = status.HTTP_413_CONTENT_TOO_LARGE

Review Comment:
   Done in 3f8fdc6a20 — dropped the 413 path entirely, always 422 now.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to