From be7ebecfd5040a0820f433cc06c914ae885d5564 Mon Sep 17 00:00:00 2001 From: mohanedmoh <19634204+mohanedmoh@users.noreply.github.com> Date: Wed, 7 Oct 2026 09:16:31 +0100 Subject: [PATCH 1/2] fix: return stable error detail for out-of-range datetimes in str_to_interval iso8601.ParseError forwards the Python-version-dependent datetime error message. Raise a fixed ValueError instead so the 400 detail is the same on every Python version. --- CHANGES.md | 1 + stac_fastapi/types/stac_fastapi/types/rfc3339.py | 6 +++++- stac_fastapi/types/tests/test_rfc3339.py | 14 ++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 07a80ad05..f4222417d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ - Return a `Location` header with the URI of the newly created resource for `POST /collections/{collection_id}/items` (single `Item`) and `POST /collections` in the Transaction extension, as required by the spec. `ItemCollection` payloads still return 201 without a `Location` header. The header is also added when a client returns a `Response` object directly, unless it already sets `Location` or returns an error status. ([#982](https://github.com/stac-utils/stac-fastapi/pull/982)) - Type `PartialCollection.links` as a list so `PATCH /collections/{collection_id}` merge patches carrying a `links` array are no longer rejected with 400. - Declare `ItemCollection` as a valid response of `POST /collections/{collection_id}/items` (route response model and `create_item` client return type) so a successful ItemCollection create returns 201 instead of 500 when `enable_response_models` is on. +- Return a stable `Invalid RFC3339 datetime.` error detail from `str_to_interval` for out-of-range datetime values instead of the Python-version-dependent message. ([#991](https://github.com/stac-utils/stac-fastapi/pull/991)) ## [7.0.0] - 2026-09-09 diff --git a/stac_fastapi/types/stac_fastapi/types/rfc3339.py b/stac_fastapi/types/stac_fastapi/types/rfc3339.py index f69ad3d87..8cde095fe 100644 --- a/stac_fastapi/types/stac_fastapi/types/rfc3339.py +++ b/stac_fastapi/types/stac_fastapi/types/rfc3339.py @@ -67,7 +67,11 @@ def rfc3339_str_to_datetime(s: str) -> datetime: raise ValueError("Invalid RFC3339 datetime.") # Parse with pyiso8601 - return iso8601.parse_date(s) + try: + return iso8601.parse_date(s) + except iso8601.ParseError: + # pyiso8601 forwards the Python-version-dependent datetime error message + raise ValueError("Invalid RFC3339 datetime.") from None def parse_single_date(date_str: str) -> datetime: diff --git a/stac_fastapi/types/tests/test_rfc3339.py b/stac_fastapi/types/tests/test_rfc3339.py index 3b1ea2c5d..396c0d6aa 100644 --- a/stac_fastapi/types/tests/test_rfc3339.py +++ b/stac_fastapi/types/tests/test_rfc3339.py @@ -103,6 +103,20 @@ def test_str_to_interval_with_invalid_datetime(test_input): ) +@pytest.mark.parametrize( + "test_input", + [ + "2020-13-01T00:00:00Z", # month out of range + "2020-01-01T25:00:00Z", # hour out of range + "2020-01-01T00:00:00Z/2020-01-32T00:00:00Z", # day out of range in interval + ], +) +def test_str_to_interval_out_of_range_detail_is_stable(test_input): + with pytest.raises(HTTPException) as exc_info: + str_to_interval(test_input) + assert exc_info.value.detail == "Invalid RFC3339 datetime." + + @pytest.mark.parametrize("test_input", valid_intervals) def test_str_to_interval_with_valid_interval(test_input): assert isinstance(str_to_interval(test_input), tuple), ( From 3a393a400b00580e6d5c7960dcdb26db300ec7d5 Mon Sep 17 00:00:00 2001 From: mohanedmoh <19634204+mohanedmoh@users.noreply.github.com> Date: Thu, 8 Oct 2026 19:46:28 +0100 Subject: [PATCH 2/2] fix: keep original ParseError in exception chain --- stac_fastapi/types/stac_fastapi/types/rfc3339.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/stac_fastapi/types/stac_fastapi/types/rfc3339.py b/stac_fastapi/types/stac_fastapi/types/rfc3339.py index 8cde095fe..ceba09a65 100644 --- a/stac_fastapi/types/stac_fastapi/types/rfc3339.py +++ b/stac_fastapi/types/stac_fastapi/types/rfc3339.py @@ -69,9 +69,10 @@ def rfc3339_str_to_datetime(s: str) -> datetime: # Parse with pyiso8601 try: return iso8601.parse_date(s) - except iso8601.ParseError: - # pyiso8601 forwards the Python-version-dependent datetime error message - raise ValueError("Invalid RFC3339 datetime.") from None + except iso8601.ParseError as e: + # pyiso8601 forwards the Python-version-dependent datetime error message, + # so the public message is fixed and the original error stays in the chain + raise ValueError("Invalid RFC3339 datetime.") from e def parse_single_date(date_str: str) -> datetime: