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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

*
* Guard against None parsed URL in HTTPProtocol.send_request when connection closes during SSL handshake

## [1.38.2] - 2026-04-08

Expand Down
13 changes: 12 additions & 1 deletion src/netius/clients/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,12 @@ def send_request(self, callback=None):
parsed = self.parsed
safe = self.safe

# in case the parsed URL is not available (connection was closed
# before the request could be sent) closes the protocol to avoid
# errors in the request sending process
Comment on lines +737 to +739
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says this branch "closes the protocol", but the code only returns early. Either update the comment to match the behavior, or actually close the protocol here (and consider whether any pending callback/request state should be finalized when parsed is None).

Suggested change
# in case the parsed URL is not available (connection was closed
# before the request could be sent) closes the protocol to avoid
# errors in the request sending process
# in case the parsed URL is not available (for example, if the
# connection was closed before the request could be sent) aborts
# sending the request to avoid errors in the request process

Copilot uses AI. Check for mistakes.
if parsed == None:
return
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if parsed.query:
path += "?" + parsed.query

Expand Down Expand Up @@ -1269,7 +1275,12 @@ def method(
# notice that the event loop is also re-used accordingly
key = cls.protocol.key_g(url)
protocol = self.available.pop(key, None)
if protocol and (not protocol.is_open() or protocol.transport().is_closing()):
if protocol and (
not protocol.is_open()
or not protocol.transport()
or protocol.transport().is_closing()
or (hasattr(protocol, "is_closing") and protocol.is_closing())
):
protocol.traced("Discarding stale protocol")
protocol = None
if protocol:
Expand Down
65 changes: 65 additions & 0 deletions src/netius/test/base/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,68 @@ def test_write_closing(self):
self.assertEqual(connection.is_closed(), True)

transport.write(b"")

def test_is_closing_no_connection(self):
transport = netius.Transport(None, None, open=False)

self.assertEqual(transport._connection, None)
self.assertEqual(transport.is_closing(), True)

def test_is_closing_open_connection(self):
connection = netius.Connection()
transport = netius.Transport(None, connection)

self.assertEqual(transport.is_closing(), False)
self.assertEqual(connection.is_closed(), False)

def test_is_closing_closed_connection(self):
connection = netius.Connection()
transport = netius.Transport(None, connection)

connection.status = netius.CLOSED

self.assertEqual(transport.is_closing(), True)
self.assertEqual(connection.is_closed(), True)

def test_is_closing_protocol_closing(self):
connection = netius.Connection()
transport = netius.Transport(None, connection)

protocol = netius.Protocol()
protocol._open = True
transport._protocol = protocol

self.assertEqual(transport.is_closing(), False)
self.assertEqual(protocol.is_closing(), False)

protocol._closing = True

self.assertEqual(transport.is_closing(), False)
self.assertEqual(protocol.is_closing(), True)
Comment on lines +78 to +92
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new is_closing() tests cover the return value when a protocol is marked closing, but they don't cover the most important behavioral contract: calling protocol.close() should still result in the underlying transport/connection being closed. Adding a regression test for Protocol.close() -> Transport.abort() closing the connection would help catch the interaction introduced by the new protocol-aware is_closing() logic.

Copilot uses AI. Check for mistakes.

def test_is_closing_protocol_not_closing(self):
connection = netius.Connection()
transport = netius.Transport(None, connection)

protocol = netius.Protocol()
protocol._open = True
transport._protocol = protocol

self.assertEqual(transport.is_closing(), False)
self.assertEqual(protocol.is_open(), True)
self.assertEqual(protocol.is_closing(), False)

def test_is_closing_no_protocol(self):
connection = netius.Connection()
transport = netius.Transport(None, connection)

self.assertEqual(transport._protocol, None)
self.assertEqual(transport.is_closing(), False)

def test_is_closing_protocol_no_is_closing(self):
connection = netius.Connection()
transport = netius.Transport(None, connection)

transport._protocol = object()

self.assertEqual(transport.is_closing(), False)
47 changes: 47 additions & 0 deletions src/netius/test/clients/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,53 @@
import netius.clients


class HTTPProtocolTest(unittest.TestCase):

def test_send_request_parsed_none(self):
protocol = netius.clients.HTTPProtocol(
"GET", "http://example.com/", asynchronous=True
)

self.assertNotEqual(protocol.parsed, None)

protocol.parsed = None

result = protocol.send_request()
self.assertEqual(result, None)

def test_send_request_parsed_valid(self):
protocol = netius.clients.HTTPProtocol(
"GET", "http://example.com/path", asynchronous=True
)

self.assertNotEqual(protocol.parsed, None)
self.assertEqual(protocol.parsed.hostname, "example.com")
self.assertEqual(protocol.parsed.path, "/path")

def test_close_c_clears_parsed(self):
protocol = netius.clients.HTTPProtocol(
"GET", "http://example.com/", asynchronous=True
)

self.assertNotEqual(protocol.parsed, None)

protocol.close_c()

self.assertEqual(protocol.parsed, None)

def test_close_c_send_request_safe(self):
protocol = netius.clients.HTTPProtocol(
"GET", "http://example.com/", asynchronous=True
)

protocol.close_c()

self.assertEqual(protocol.parsed, None)

result = protocol.send_request()
self.assertEqual(result, None)


class HTTPClientTest(unittest.TestCase):

def setUp(self):
Expand Down