feat: add ApifyApiError subclasses grouped by HTTP status#737
feat: add ApifyApiError subclasses grouped by HTTP status#737
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #737 +/- ##
==========================================
+ Coverage 95.36% 95.49% +0.12%
==========================================
Files 45 45
Lines 5118 5152 +34
==========================================
+ Hits 4881 4920 +39
+ Misses 237 232 -5
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
Let me guys know whether you think this is worth it. Currently, I'm slightly more inclined not to do it and keep the status quo. Since the API type errors are just general strings, they can often change. |
Arguments in favor
Arguments against
|
|
I would add one convenience argument against this change, which would only be used as a tie breaker: Adding this feature is non-breaking. Removing it is breaking. So I would wait until we are more certain about it, as we can easily add this in any minor release. |
Addresses #423. Each `ErrorType` enum member from the OpenAPI spec now has a matching `ApifyApiError` subclass (e.g. `RecordNotFoundError`) generated into `_generated_errors.py`. `ApifyApiError.__new__` dispatches to the right subclass based on the response's `error.type`, so `except ApifyApiError` keeps working while `except RecordNotFoundError` becomes possible. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d __init__ `__new__` used to parse `response.json()` for dispatch and `__init__` parsed it again for attribute assignment. Now `__new__` stashes the parsed `error` dict on the instance and `__init__` reads it, so each raised error parses the body once. Also trims the stale star-import comment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
189e0aa to
cb57e87
Compare
There was a problem hiding this comment.
Pull request overview
This PR introduces HTTP-status-based ApifyApiError subclasses so callers can catch specific API failure modes (e.g., NotFoundError for 404) without relying on endpoint-specific error.type strings.
Changes:
- Added
ApifyApiError.__new__dispatch to map selected HTTP statuses to dedicated exception subclasses (and 5xx toServerError). - Refactored API error-body parsing into
_extract_error_payload()and kepttype/message/dataas metadata. - Updated
catch_not_found_or_throw()and unit tests to useNotFoundError/ status-based behavior.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/apify_client/errors.py |
Adds status→exception dispatch and new ApifyApiError subclasses; refactors payload extraction. |
src/apify_client/_utils.py |
Updates not-found suppression to use isinstance(..., NotFoundError). |
tests/unit/test_utils.py |
Adjusts not-found suppression expectations and mocks JSON payload parsing. |
tests/unit/test_client_errors.py |
Adds tests asserting correct subclass dispatch for mapped statuses, streamed responses, 5xx, and fallback cases. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| is_not_found_status = exc.status_code == HTTPStatus.NOT_FOUND | ||
| is_not_found_type = exc.type in ['record-not-found', 'record-or-token-not-found'] | ||
| if not (is_not_found_status and is_not_found_type): | ||
| if not isinstance(exc, NotFoundError): |
There was a problem hiding this comment.
I am not sure about this (but the previous variant had the same issues.)
The problem of swallowing 404s is that you lose the information about what was not found. For example
dataset_1 = client.run(run_id="RUN DOES NOT EXIST").dataset().get() # Run does not exist
dataset_2 = client.run(run_id="RUN EXISTS").dataset().get() # Dataset does not exist
In both cases it returns None and user has no idea which of the cases happened.
There was a problem hiding this comment.
We should probably just swallow 404 related to the last resource in the chain and throw all the others?
Summary
Addresses #423. Adds a small set of
ApifyApiErrorsubclasses so callers can narrowexceptclauses to specific failure modes. Dispatch is driven by HTTP status — a stable contract — rather than the per-endpointerror.typestring.type,message, anddataremain exposed as metadata.All subclasses inherit from
ApifyApiError, so existingexcept ApifyApiErrorhandlers keep matching.InvalidRequestErrorUnauthorizedErrorForbiddenErrorNotFoundErrorConflictErrorRateLimitErrorServerErrorUnmapped statuses and unparsable bodies fall back to the base
ApifyApiError.Migration note
Constructing
ApifyApiError(response, ...)directly now returns an instance of the matching subclass.except ApifyApiErroris unaffected; tests assertingtype(exc) is ApifyApiErroron a mapped status would need updates.catch_not_found_or_thrownow usesisinstance(exc, NotFoundError)instead of a hardcodederror.typeallowlist.