Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/apify_client/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,16 @@ class ApifyApiError(ApifyClientError):

def __new__(cls, response: HttpResponse, attempt: int, *, method: str = 'GET') -> Self: # noqa: ARG004
"""Dispatch to the subclass matching the response's HTTP status code, if any."""
target_cls: type[ApifyApiError] = cls
instance = super().__new__(cls)
if cls is ApifyApiError:
status = response.status_code
mapped = _STATUS_TO_CLASS.get(status)
if mapped is None and status >= HTTPStatus.INTERNAL_SERVER_ERROR:
mapped = ServerError
if mapped is not None:
target_cls = mapped
return super().__new__(target_cls)
# Retagging is runtime-equivalent to allocating `mapped` directly, but keeps the return type `Self`.
instance.__class__ = mapped
return instance

def __init__(self, response: HttpResponse, attempt: int, *, method: str = 'GET') -> None:
"""Initialize the API error from a failed response.
Expand Down
15 changes: 15 additions & 0 deletions tests/unit/test_client_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import json
from typing import TYPE_CHECKING, Any
from unittest.mock import Mock

import pytest
from werkzeug import Response
Expand Down Expand Up @@ -243,6 +244,20 @@ def test_apify_api_error_dispatches_all_mapped_statuses(
assert exc.value.status_code == status_code


def test_apify_api_error_subclass_constructed_directly_keeps_its_class() -> None:
"""Only `ApifyApiError` itself dispatches - a subclass constructed directly is never re-dispatched by status."""
response = Mock()
response.status_code = 404
response.json.return_value = {'error': {'type': 'record-not-found', 'message': 'nope'}}

# 404 maps to `NotFoundError`, but the explicit class must win over the status.
error = ServerError(response, 1)

assert type(error) is ServerError
assert error.status_code == 404
assert error.type == 'record-not-found'


def test_apify_api_error_falls_back_for_unparsable_body(httpserver: HTTPServer) -> None:
"""When the body can't be parsed, status-based dispatch still applies and `.type` is None."""
httpserver.expect_request('/unparsable').respond_with_data('<not json>', status=418, content_type='text/html')
Expand Down
Loading
Loading