From 5da56a362cd98df4ce9c0865084e74e1118dc268 Mon Sep 17 00:00:00 2001 From: hideyukiMORI Date: Wed, 20 May 2026 00:42:06 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20PaginationQueryParser=20=E3=81=8C?= =?UTF-8?q?=E9=9D=9E=E6=95=B4=E6=95=B0=E3=83=91=E3=83=A9=E3=83=A1=E3=83=BC?= =?UTF-8?q?=E3=82=BF=E3=81=A7=20500=20=E3=81=A7=E3=81=AF=E3=81=AA=E3=81=8F?= =?UTF-8?q?=20ValidationException=20=E3=82=92=20raise=20=E3=81=99=E3=82=8B?= =?UTF-8?q?=20(#108)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ?limit=abc / ?offset=xyz のような非整数クエリパラメータで int() 変換が ValueError を raise していたため、ErrorHandlerMiddleware に 500 として処理されていた。 ValueError を ValidationException (field / code: "invalid") に変換して、 正しく 422 Unprocessable Entity が返るようにした。 テスト3件追加: 非整数 limit / offset / 両方同時のエラー収集を確認 Co-Authored-By: Claude Sonnet 4.6 --- src/nene2/http/pagination.py | 13 +++++++++++-- tests/nene2/http/test_pagination.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/nene2/http/pagination.py b/src/nene2/http/pagination.py index 1ddd94d..0f4a06c 100644 --- a/src/nene2/http/pagination.py +++ b/src/nene2/http/pagination.py @@ -34,10 +34,19 @@ def parse( Raises ValidationException (→ 422) when values are out of range. """ params = request.query_params - limit = int(params.get("limit", default_limit)) - offset = int(params.get("offset", 0)) errors: list[ValidationError] = [] + try: + limit = int(params.get("limit", default_limit)) + except ValueError: + errors.append(ValidationError("limit", "limit must be an integer.", "invalid")) + limit = default_limit + try: + offset = int(params.get("offset", 0)) + except ValueError: + errors.append(ValidationError("offset", "offset must be an integer.", "invalid")) + offset = 0 + if limit < 1 or limit > max_limit: errors.append( ValidationError( diff --git a/tests/nene2/http/test_pagination.py b/tests/nene2/http/test_pagination.py index ecbb315..d18cb26 100644 --- a/tests/nene2/http/test_pagination.py +++ b/tests/nene2/http/test_pagination.py @@ -43,6 +43,34 @@ def test_limit_out_of_range_raises() -> None: assert exc_info.value.errors[0].field == "limit" +def test_non_integer_limit_raises_validation_exception() -> None: + mock_request = MagicMock(spec=Request) + mock_request.query_params = {"limit": "abc"} + with pytest.raises(ValidationException) as exc_info: + PaginationQueryParser.parse(mock_request) + assert exc_info.value.errors[0].field == "limit" + assert exc_info.value.errors[0].code == "invalid" + + +def test_non_integer_offset_raises_validation_exception() -> None: + mock_request = MagicMock(spec=Request) + mock_request.query_params = {"offset": "xyz"} + with pytest.raises(ValidationException) as exc_info: + PaginationQueryParser.parse(mock_request) + assert exc_info.value.errors[0].field == "offset" + assert exc_info.value.errors[0].code == "invalid" + + +def test_both_invalid_collects_all_errors() -> None: + mock_request = MagicMock(spec=Request) + mock_request.query_params = {"limit": "abc", "offset": "xyz"} + with pytest.raises(ValidationException) as exc_info: + PaginationQueryParser.parse(mock_request) + fields = [e.field for e in exc_info.value.errors] + assert "limit" in fields + assert "offset" in fields + + def test_pagination_response_without_total() -> None: r = PaginationResponse(items=[{"id": 1}], limit=20, offset=0) d = r.to_dict()