Repository navigation
ConnectionResetError won't be identified as retryable #1344
Copy link
Copy link
Closed
Labels
P1High priorityHigh priorityautomated-testingRelated to automated testing toolsRelated to automated testing toolsbugSomething isn't workingSomething isn't working
Description
Some of our tests are failing with:
query_and_describe attempt 1 from PID 8 to GET /versions/us.shared_airspace failed with non-retryable ConnectionError: ('Connection aborted.', ConnectionResetError(104, 'Connection reset by peer'))
#1310 is trying to fix an issue to catch ConnectionResetError so they can be retried.
But it seems it will miss this error anyway, it needs to also traverse exception.args - to find exceptions wrapped as constructor arguments, not just exceptions in the chain:
from http.client import RemoteDisconnected
def context_contains(exception, types) -> bool:
if exception.__class__ in types:
return True
parent = getattr(exception, '__context__', None)
if parent:
return context_contains(parent, types)
else:
return False
# Simulate the actual error our the logs
inner = ConnectionResetError(104, 'Connection reset by peer')
e = ConnectionError('Connection aborted.', inner)
print(f'e.__context__: {e.__context__}')
print(f'e.args: {e.args}')
print(f'Retryable with code: {context_contains(e, (RemoteDisconnected, ConnectionResetError))}')e.context: None
e.args: ('Connection aborted.', ConnectionResetError(104, 'Connection reset by peer'))
Retryable with code: False
The ConnectionResetError is in args, not context.
from http.client import RemoteDisconnected
def context_contains_fixed(exception, types) -> bool:
if exception.__class__ in types:
return True
parent = getattr(exception, '__context__', None)
if parent:
if context_contains_fixed(parent, types):
return True
# Also check args for wrapped exceptions
for arg in getattr(exception, 'args', []):
if isinstance(arg, BaseException):
if context_contains_fixed(arg, types):
return True
return False
inner = ConnectionResetError(104, 'Connection reset by peer')
e = ConnectionError('Connection aborted.', inner)
print(f'e.__context__: {e.__context__}')
print(f'e.args: {e.args}')
print(f'Retryable with code: {context_contains_fixed(e, (RemoteDisconnected, ConnectionResetError))}')e.context: None
e.args: ('Connection aborted.', ConnectionResetError(104, 'Connection reset by peer'))
Retryable with code: True
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
P1High priorityHigh priorityautomated-testingRelated to automated testing toolsRelated to automated testing toolsbugSomething isn't workingSomething isn't working
Activity
BenjaminPelletier commentedon Feb 13, 2026
@the-glu do you think you could take an initial look?
the-glu commentedon Feb 16, 2026
I'm not 100% sure what is happening there, when testing on my side with a "real" error is does works:
Assuming a server with
I'm getting the following with the original function:
Maybe there are different path to those connections reset? Do you have a way that I can reproduce that connection reset ?
akhenakh commentedon Feb 16, 2026
I looked into it because of the error we received from the test conducted remotely, without knowing the current version was different.
Since we can't control the environment there, maybe it's a different version of requests which behaves differently 🤷♂️
the-glu commentedon Feb 16, 2026
Python and requests' version are (in theory) locked to specific version, so that should hopefully not happens, but there may be an issue somewhere.
Witch commit are you using? (Latest release don't have #1310, it's only in main' branch) How to you run the tests? (Docker image?)
akhenakh commentedon Feb 16, 2026
We are not the one conducting the tests, we are the one tested.
akhenakh commentedon Feb 16, 2026
I think your test is different cause it is not reusing a connection from the pool since it did not complete a request first.
urllib3 raises ProtocolError requests catches it and raises ConnectionError
But in the real code the reset happens after the connection is established and pooled so the path to raise is different.
MaxRetryError is in context but ProtocolError (with ConnectionResetError) is in MaxRetryError.reason
the ProtocolError with ConnectionResetError inside is in MaxRetryError.reason, not in context.
the-glu commentedon Feb 16, 2026
I'm not sure I understand: I don't think we use connection pooling in the test suite.
Can you please provide the version you do use? That would help a lot to narrow the issue.
If you have access to the tests report, it's recorded in the last two properties:
akhenakh commentedon Feb 16, 2026
class UTMClientSession(requests.Session) requests.Session is creating a http connection pool
the-glu commentedon Feb 16, 2026
True, but even in a reused pooled connection, the behavior is the same:
akhenakh commentedon Feb 16, 2026
Here is the version visible from the test:
Codebase version: interuss/monitoring/v0.25.0
Commit hash:
a8adabe
the-glu commentedon Feb 16, 2026
Ok, so that normal, the fix from #1310 haven't been released in v0.25.0.
Can you switch to the main branch or request a new release (there is a template when you create an issue) to check if that indeed fix the issue in your case?
akhenakh commentedon Feb 16, 2026
Yes we found out it was not running the right version but I was referring to this below because I'm not sure it will catch those:
https://github.com/urllib3/urllib3/blob/main/src/urllib3/connectionpool.py#L838-L839
This results in an exception object where .args is ('Connection aborted.', ConnectionResetError(...)).
Then the requests library wraps this ProtocolError inside a ConnectionError creating the nested structure:
https://github.com/psf/requests/blob/main/src/requests/adapters.py#L660-L661
it's wrapped twice (only checked on recent versions).
akhenakh commentedon Feb 16, 2026
Because I did not know it was running a different version, and because I had to check for errors in args using requests in the past, I was convinced it was the issue (wrongfully), so I digged into it and found those, that could be a legit issue, but it's a shot in the dark since, again, we are not the ones conducting the tests and handling the versions.
the-glu commentedon Feb 16, 2026
Yes it's wrapped, but as I understand it,
__context__is always set to the 'original' exception ( https://docs.python.org/3/library/exceptions.html#BaseException.__context__ )Meaning from your links, that the
ProtocolErrorwill have theeas context (and being raised inretries.increment, and theConnectionErrorwill have theProtocolErroras context, regardless ofargs, so I would say it should be catched correctly.As long as we don't have the issue in practice, it should be fine no?
akhenakh commentedon Feb 16, 2026
I think the problem is with urllib3
new_e = ProtocolError("Connection aborted.", new_e)is instantiated it here but is not raised right away so__context__wont be set there.I agree we need to test the current patch as soon as possible anyway.
We have this thread in case the issue is coming back.
the-glu commentedon Feb 17, 2026
I let you reopen this issue if you still have the issue with the current codebase.