Repository navigation
fix!: errors and redaction (pickling, query masking, DecodeError message, read-only STATUS_TO_EXCEPTION) - #166
Merged
Merged
Conversation
One __reduce__ on ClientError rebuilds from args without calling __init__ and restores __dict__, replacing _KeywordReduceMixin and the StatusError reconstructor. add_note() notes and user-set attributes now survive pickle, copy.copy and copy.deepcopy for every error class.
STATUS_TO_EXCEPTION is now a types.MappingProxyType. Code that assigned to it to remap a status now gets TypeError; catch the error in a middleware and raise your own class instead.
redact_url now replaces the value of every query and fragment parameter with REDACTED and keeps the keys, instead of masking an allowlist of sensitive names, so presigned-URL signatures and other unlisted secrets no longer reach messages, reprs, log records or span events. Userinfo is cut at the last @ so a raw password containing @ is stripped whole.
The message is now 'failed to decode response into <model>: <type>' with the error count appended when the original exception has error_count(). A non-class model such as X | None is shown by repr, which stops the AttributeError on Python 3.11 to 3.13. exc.original is unchanged.
repr() of a middleware built by before_request, after_response, on_error or their async siblings no longer raises AttributeError for a functools.partial or a callable instance; it falls back to repr(f).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
Pickling and copying
ClientErrornow has a single__reduce__. It rebuilds the instance withcls.__new__(cls, *args), skipping__init__, and restores__dict__. This replaces_KeywordReduceMixin,_reconstruct_kwonlyand_reconstruct_status_error. Before,pickle,copy.copyandcopy.deepcopylostadd_note()notes on everyStatusErrorand raisedTypeError(unexpected keyword__notes__) on the six keyword-only classes. A user-set attribute broke them the same way. That hits anything that ships exceptions across processes, such as a Celery worker or amultiprocessingpool, and retry already callsadd_note()on the exceptions it gives up on. A new class no longer needs pickling code, just picklable attributes. The old invariant test that checked__dict__against the__init__keywords had nothing left to guard, so it is gone. Two tests replace it: a round trip over every exported error class (pickle, copy and deepcopy, each with a note and a user attribute), and an invariant that every exportedClientErrorclass has a case in that round trip. Theerrors.pybullet in AGENTS.md now describes this rule.Redaction
redact_urlreplaces every query-parameter value withREDACTEDand keeps the keys as written, so?page=2&X-Amz-Signature=abcbecomes?page=REDACTED&X-Amz-Signature=REDACTED. The old allowlist missed presigned-URL signatures and any other secret under a name it did not know. Fragment values get the same treatment, and a fragment with no=in it, such as#section-3, is kept. A bare query key like?flaghas no value and stays as it is. Userinfo is still stripped, now cut at the last@(rpartition), so a rawuser:p@ss@hostno longer leavesss@behind. httpx2 percent-encodes@in userinfo, so this only matters for raw strings passed toredact_url. Keys are no longer re-encoded throughurlencode. The unredacted URL is still onexc.response.request.url.Callers:
StatusError.__str__/__repr__(errors.py) and_internal/observability._emit_event, which redacts theurlattribute of every event. All resilience events (retry, bulkhead, circuit breaker, timeout) go through_emit_event, and nothing builds aurlattribute or exception message around it.DecodeErrormessageThe message is now
failed to decode response into <model>: <OriginalType>, with(<n> errors)appended when the original has anerror_count()method (pydantic'sValidationError; the count is singular for one error). It used to embedstr(original), and pydantic puts the rejected input values in that string, so response body content ended up in the message.<model>ismodel.__name__for a class andrepr(model)otherwise. That fixes theAttributeErroron Python 3.11 to 3.13 whenresponse_modelisX | None, andlist[X]no longer shows aslist.exc.originalis unchanged.STATUS_TO_EXCEPTION
STATUS_TO_EXCEPTIONis atypes.MappingProxyType, so assigning to it raisesTypeError.Phase decorator reprs
The phase decorators' reprs use
getattr(f, '__qualname__', repr(f)), sorepr()of a middleware built from afunctools.partialor a callable instance no longer raises. The sixty: ignorecomments are gone.Behaviour changes for the 0.22.0 release notes
STATUS_TO_EXCEPTIONis read-only. Code that assigned to it to remap a status now getsTypeError. To raise your own class for a status, catch the httpware error in a middleware (for example anon_errorhandler) and raise yours from it. Subclassing an httpware error does not remap anything by itself.REDACTED. Log queries or alerts that matched on query values in theurlfield or instr(exc)need updating.str(DecodeError)no longer contains the decoder's own message. Readexc.originalwhen you need it.Test evidence
Each fix started with a failing test:
test_error_round_trip_keeps_notes_and_attributes: 54 of 66 cases failed, withAttributeError: 'NotFoundError' object has no attribute '__notes__'on the status classes andTypeError: DecodeError.__init__() got an unexpected keyword argument '__notes__'on the keyword-only ones.test_status_to_exception_cannot_be_mutated:Failed: DID NOT RAISE TypeError.test_redact_url[...page=2&limit=10...]and its neighbours,test_status_error_message_masks_every_query_value, and bothtest_emit_event_redacts_url_secret_*tests failed with the values still in place. The rpartition case failed with'https://ss@example.test/p' == 'https://example.test/p'.test_decode_error_summary_*tests failed with the pydantic message (1 validation error for _DecodeErrorModel ...) instr(exc). On 3.14X | Nonerendered asUnionandlist[X]aslist. The same test on 3.13 raisedAttributeError: 'types.UnionType' object has no attribute '__name__'.test_decorator_repr_falls_back_to_repr_without_qualname(sync and async): 12 failures,AttributeError: 'functools.partial' object has no attribute '__qualname__'and the same for a callable instance.just lint,just lint-ciandjust testpass: 1087 tests on 3.14, and the full suite also passes on 3.13.Performance
timeiton CPython 3.14.7, best of 7, ns per call, before and after:redact_url, no queryredact_url,?page=2&limit=50&sort=nameredact_url, query withapi_keyredact_url, presigned S3 URLredact_url, userinfo plus queryNotFoundError(response), URL with queryDecodeError(...)with a pydanticValidationErrorMasking reuses the string pieces instead of running
parse_qslandurlencode, and returns early when the URL has no=.