diff --git a/.chronus/changes/python-custom-error-status-mapping-2026-6-16-9-52-50.md b/.chronus/changes/python-custom-error-status-mapping-2026-6-16-9-52-50.md new file mode 100644 index 00000000000..520072bfca1 --- /dev/null +++ b/.chronus/changes/python-custom-error-status-mapping-2026-6-16-9-52-50.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/http-client-python" +--- + +Always populate the operation `error_map` with the standard azure-core error types (401 → `ClientAuthenticationError`, 404 → `ResourceNotFoundError`, 409 → `ResourceExistsError`, 304 → `ResourceNotModifiedError`), even when a customized error model covers those status codes. Previously, a standard status code covered by a customized ranged or default error model fell back to a generic `HttpResponseError`; it now raises its dedicated error type via `map_error`. The customized error body continues to be deserialized and attached to the `HttpResponseError` raised for other (non-standard) status codes. diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py index c615baf827f..a079cf23e44 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py @@ -1088,25 +1088,12 @@ def handle_error_response( # pylint: disable=too-many-statements, too-many-bran " )", ] ) - # add build-in error type - # TODO: we should decide whether need to this wrapper for customized error type - status_code_error_map = { - 401: "ClientAuthenticationError", - 404: "ResourceNotFoundError", - 409: "ResourceExistsError", - 304: "ResourceNotModifiedError", - } - if status_code in status_code_error_map: - retval.append( - " raise {}(response=response{}{})".format( - status_code_error_map[cast(int, status_code)], - error_model, - (", error_format=ARMErrorFormat" if self.code_model.options["azure-arm"] else ""), - ) - ) - condition = "if" - else: - condition = "elif" + # The dedicated azure-core error type for a standard status + # code is raised by ``map_error`` via the error map, so here + # we only deserialize the customized error body (used by the + # generic ``HttpResponseError`` fallback for non-standard + # status codes within the response). + condition = "elif" # ranged status code only exist in typespec and will not have multiple status codes else: retval.append( @@ -1238,36 +1225,17 @@ def handle_response(self, builder: OperationType) -> list[str]: retval.append("return 200 <= response.status_code <= 299") return retval - def _need_specific_error_map(self, code: int, builder: OperationType) -> bool: - for non_default_error in builder.non_default_errors: - # single status code - if code in non_default_error.status_codes: - return False - # ranged status code - if ( - isinstance(non_default_error.status_codes[0], list) - and non_default_error.status_codes[0][0] <= code <= non_default_error.status_codes[0][1] - ): - return False - return True - def error_map(self, builder: OperationType) -> list[str]: retval = ["error_map: MutableMapping = {"] - if builder.non_default_errors and self.code_model.options["models-mode"]: - # TODO: we should decide whether to add the build-in error map when there is a customized default error type - if self._need_specific_error_map(401, builder): - retval.append(" 401: ClientAuthenticationError,") - if self._need_specific_error_map(404, builder): - retval.append(" 404: ResourceNotFoundError,") - if self._need_specific_error_map(409, builder): - retval.append(" 409: ResourceExistsError,") - if self._need_specific_error_map(304, builder): - retval.append(" 304: ResourceNotModifiedError,") - else: - retval.append( - " 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError, " - "304: ResourceNotModifiedError" - ) + # Always map the standard status codes to their dedicated azure-core error + # type so ``map_error`` raises the correct semantic error, even when a + # customized error model (single, ranged, or default) covers them. The + # customized error body is still deserialized in ``handle_error_response`` + # for the generic ``HttpResponseError`` fallback. + retval.append( + " 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError, " + "304: ResourceNotModifiedError" + ) retval.append("}") if builder.has_etag: retval.extend( diff --git a/packages/http-client-python/tests/mock_api/shared/asynctests/test_response_status_code_range_async.py b/packages/http-client-python/tests/mock_api/shared/asynctests/test_response_status_code_range_async.py index 44118e03b0a..3c7ffe49cef 100644 --- a/packages/http-client-python/tests/mock_api/shared/asynctests/test_response_status_code_range_async.py +++ b/packages/http-client-python/tests/mock_api/shared/asynctests/test_response_status_code_range_async.py @@ -6,7 +6,7 @@ import pytest import pytest_asyncio from response.statuscoderange.aio import StatusCodeRangeClient -from response.statuscoderange.models import ErrorInRange, NotFoundError +from response.statuscoderange.models import ErrorInRange @pytest_asyncio.fixture @@ -29,11 +29,9 @@ async def test_error_response_status_code_in_range(client: StatusCodeRangeClient @pytest.mark.asyncio async def test_error_response_status_code_404(client: StatusCodeRangeClient, core_library): + # 404 maps to the dedicated azure-core ``ResourceNotFoundError`` via ``map_error``, + # which raises before the customized error body is deserialized, so no model is attached. with pytest.raises(core_library.exceptions.ResourceNotFoundError) as exc_info: await client.error_response_status_code404() - error = exc_info.value.model - assert isinstance(error, NotFoundError) - assert error.code == "not-found" - assert error.resource_id == "resource1" assert exc_info.value.response.status_code == 404 diff --git a/packages/http-client-python/tests/mock_api/shared/test_response_status_code_range.py b/packages/http-client-python/tests/mock_api/shared/test_response_status_code_range.py index 872f0b1eeeb..b4d0295175c 100644 --- a/packages/http-client-python/tests/mock_api/shared/test_response_status_code_range.py +++ b/packages/http-client-python/tests/mock_api/shared/test_response_status_code_range.py @@ -5,7 +5,7 @@ # -------------------------------------------------------------------------- import pytest from response.statuscoderange import StatusCodeRangeClient -from response.statuscoderange.models import ErrorInRange, NotFoundError +from response.statuscoderange.models import ErrorInRange @pytest.fixture @@ -26,11 +26,9 @@ def test_error_response_status_code_in_range(client: StatusCodeRangeClient, core def test_error_response_status_code_404(client: StatusCodeRangeClient, core_library): + # 404 maps to the dedicated azure-core ``ResourceNotFoundError`` via ``map_error``, + # which raises before the customized error body is deserialized, so no model is attached. with pytest.raises(core_library.exceptions.ResourceNotFoundError) as exc_info: client.error_response_status_code404() - error = exc_info.value.model - assert isinstance(error, NotFoundError) - assert error.code == "not-found" - assert error.resource_id == "resource1" assert exc_info.value.response.status_code == 404