diff --git a/core/tests/unit/test_typed_metadata.py b/core/tests/unit/test_typed_metadata.py index 198c3397..177cd1ed 100644 --- a/core/tests/unit/test_typed_metadata.py +++ b/core/tests/unit/test_typed_metadata.py @@ -157,6 +157,18 @@ def test_number_coercion_rejects_nan_and_infinity(self): with pytest.raises(TypedMetadataError, match="cannot store NaN or infinite"): _normalize_values({"value": float("inf")}, {"value": "number"}) + def test_number_coercion_rejects_nan_and_infinity_strings(self): + """Non-finite values supplied as strings must be rejected too. + + Regression: the string branch parsed "inf"/"nan" (and overflowing + literals like "1e400") into non-finite floats without the finite check + applied to numeric inputs, letting them through to storage where they + break JSON serialization and Postgres double precision columns. + """ + for token in ("inf", "-inf", "Infinity", "nan", "1e400"): + with pytest.raises(TypedMetadataError, match="cannot store NaN or infinite"): + _normalize_values({"value": token}, {"value": "number"}) + def test_decimal_coercion(self): """Test decimal coercion from various types.""" metadata = { diff --git a/core/utils/typed_metadata.py b/core/utils/typed_metadata.py index 307dc825..c9f9ff8a 100644 --- a/core/utils/typed_metadata.py +++ b/core/utils/typed_metadata.py @@ -238,9 +238,12 @@ def _coerce_number(value: Any, field: str) -> int | float: try: if all(ch.isdigit() or ch in {"+", "-", "_"} for ch in text.replace("_", "")) and "." not in text: return int(text.replace("_", "")) - return float(text) + parsed = float(text) except ValueError as exc: # noqa: BLE001 raise TypedMetadataError(f"Metadata field '{field}' expects a numeric value.") from exc + if math.isnan(parsed) or math.isinf(parsed): + raise TypedMetadataError(f"Metadata field '{field}' cannot store NaN or infinite values.") + return parsed raise TypedMetadataError(f"Metadata field '{field}' expects a numeric value.")