-
-
Notifications
You must be signed in to change notification settings - Fork 98
Handle socket errors when sending HTTP-over-HTTPS error responses #802
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #802 +/- ##
==========================================
- Coverage 77.55% 77.55% -0.01%
==========================================
Files 41 41
Lines 4688 4692 +4
Branches 542 541 -1
==========================================
+ Hits 3636 3639 +3
- Misses 909 911 +2
+ Partials 143 142 -1 |
2aa4578 to
2e1424b
Compare
When a client sends plaintext HTTP to a TLS-only port, the SSL layer detects the invalid handshake and may close the underlying socket. The server attempts to send a 400 Bad Request error response, but the socket may already be closed, causing OSError during the flush. With pyOpenSSL, the response usually succeeds. With the builtin SSL adapter, the socket may be closed before the write can occur. This fix overrides `_flush_unlocked()` in `StreamWriter` to catch `OSError` and clear the buffer. This allows: - The explicit flush to fail gracefully when sending the 400 response - Object finalization (`__del__`) to complete without errors
2e1424b to
0540de5
Compare
| wfile.flush() | ||
| wfile.close() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
plz avoid having more than one instruction in try-blocks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, this should probably be a with-block anyway.
| We override this method because when a client sends plaintext HTTP | ||
| to a TLS-only port, SSL detects a bad handshake and may | ||
| invalidate the underlying socket. At that point, the socket | ||
| may not be writable. Attempting to write (e.g., sending a | ||
| 400 Bad Request) may succeed or raise OSError. This override | ||
| prevents OSError from propagating. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This part looks like a code comment, not a docstring.
| noqa | ||
| PIL | ||
| pipelining | ||
| plaintext |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might not need this if we don't put that comment into a docstring but have it as a comment.
| # fatal alert triggered by invalid handshake). | ||
| # Clearing the buffer prevents the error from happening | ||
| # again during cleanup. | ||
| self._write_buf.clear() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if we should be suppressing problems on the low level like this. It's likely the caller that should perform the suppression since the stream writer does not know the context it's used in (nor should it).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like that we mess with private attributes (although, the existing code already accesses it). In particular, it's weird how TLS quirks leak into makefile. I'd wait for other bits of refactoring, especially the removal of makefile in favor of classmethods before doing anything about this.
|
@julianz- did this become redundant after the last merge? Anything worth salvaging? |
|
I think this fix is still useful. I saw on my last commit on my fork for #800, "OSError: [Errno 9] Bad file descriptor" coming from one environment because |
|
The underlying issue should be fixed, yes. However, I'm not convinced that this is the right place to patch. This is not only because of the private method. It's because the stream writer/reader are generic. They don't know what they operate on, nor should they (to maintain abstraction separation). It's the caller that would know if it's working in the TLS context or plain TCP. And I'm pretty sure the calling side needs to be handling any I/O-related errors that arise from using these objects exactly where the calls are made. Suppressing such problems will influence any caller in any context, making it look like the operations they invoke complete just fine and they can proceed with whatever they were doing, potentially issuing more calls that would definitely fail. This is a recipe for disaster. The main root cause is likely mismatching lifetimes of objects (sockets vs. stream readers/writers). We seem to be forgetting to close resources after use, as you've identified in one part of the PR. So this was causing the garbage collector to invoke To address that, we should make sure such the resources are freed and discarded ( Fixing this is definitely a larger effort that should be coherent across the code base. I'd perhaps prioritize other PRs before handling this one properly. |
|
It's possible that #779 was closer to the solution partially, but incomplete. I'd still postpone this one for now, though. |
When a client sends plaintext HTTP to a TLS-only port, the SSL layer detects the invalid handshake and may close the underlying socket. The server attempts to send a 400 Bad Request error response, but the socket may already be closed, causing OSError during the flush.
With pyOpenSSL, the response usually succeeds. With the builtin SSL adapter, the socket is typically closed before the write can occur.
This fix overrides
_flush_unlocked()inStreamWriterto catchOSErrorand clear the buffer. This allows:__del__) to complete without errorsNote that swallowing OSErrors here should not affect normal communication:
write()calls upon_flush_unlocked()inStreamWriterwhich in turn calls the base class implementation_flush_unlockedin BufferedWriter. The base implementation handles errors of typeBlockingIOErrorsuch asWantWriteErrorandWantReadsError. But if the socket is dead an OSError is raised which propagates up toStreamWriterwhere_flush_unlocked()now handles the error.