Bug report
Bug description:
On macOS, asyncio server silently skips setting TCP_NODELAY on accepted connections.
Reproduce
import asyncio
import socket
async def main():
async def on_connect(reader, writer):
sock = writer.transport.get_extra_info('socket')
nodelay = sock.getsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY)
print(f'server side: sock.proto={sock.proto} TCP_NODELAY={nodelay}')
writer.close()
await writer.wait_closed()
srv.close()
srv = await asyncio.start_server(on_connect, '127.0.0.1', 0)
port = srv.sockets[0].getsockname()[1]
reader, writer = await asyncio.open_connection('127.0.0.1', port)
sock = writer.transport.get_extra_info('socket')
nodelay = sock.getsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY)
print(f'client side: sock.proto={sock.proto} TCP_NODELAY={nodelay}')
await asyncio.sleep(0.1)
writer.close()
await writer.wait_closed()
await srv.wait_closed()
asyncio.run(main())
| Platform |
Side |
sock.proto |
TCP_NODELAY |
| Linux |
client |
6 ✅ |
1 ✅ |
| Linux |
server |
6 ✅ |
1 ✅ |
| macOS |
client |
6 ✅ |
4 ✅ |
| macOS |
server |
0 ❌ |
0 ❌ |
Cause
In socket.__init__, CPython tries to detect proto with getsockopt + SO_PROTOCOL and falls back to 0 if SO_PROTOCOL is not supported, which is the case on macOS.
|
#ifdef SO_PROTOCOL |
|
if (proto == -1) { |
|
int tmp; |
|
socklen_t slen = sizeof(tmp); |
|
if (getsockopt(fd, SOL_SOCKET, SO_PROTOCOL, |
|
(void *)&tmp, &slen) == 0) |
|
{ |
|
proto = tmp; |
|
} else { |
|
set_error(); |
|
return -1; |
|
} |
|
} |
|
#else |
|
proto = 0; |
|
#endif |
While socket.accept does pass in the proto from listening socket, it is silently overridden to zero by the above logic.
|
sock = socket(self.family, self.type, self.proto, fileno=fd) |
asyncio sets TCP_NODELAY only when sock.proto == socket.IPPROTO_TCP, but it is 0 on macOS for the above reason.
|
if hasattr(socket, 'TCP_NODELAY'): |
|
def _set_nodelay(sock): |
|
if (sock.family in {socket.AF_INET, socket.AF_INET6} and |
|
sock.type == socket.SOCK_STREAM and |
|
sock.proto == socket.IPPROTO_TCP): |
|
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) |
|
else: |
|
def _set_nodelay(sock): |
|
pass |
Client socket works fine because no fd is passed to socket.__init__ so the proto detection doesn't happen.
CPython versions tested on:
3.13
Operating systems tested on:
macOS, Linux
Linked PRs
Bug report
Bug description:
On macOS, asyncio server silently skips setting
TCP_NODELAYon accepted connections.Reproduce
sock.protoTCP_NODELAYCause
In
socket.__init__, CPython tries to detect proto withgetsockopt+SO_PROTOCOLand falls back to 0 ifSO_PROTOCOLis not supported, which is the case on macOS.cpython/Modules/socketmodule.c
Lines 5749 to 5764 in 8276778
While
socket.acceptdoes pass in theprotofrom listening socket, it is silently overridden to zero by the above logic.cpython/Lib/socket.py
Line 300 in 8276778
asynciosetsTCP_NODELAYonly whensock.proto == socket.IPPROTO_TCP, but it is 0 on macOS for the above reason.cpython/Lib/asyncio/base_events.py
Lines 193 to 201 in 8276778
Client socket works fine because no fd is passed to
socket.__init__so the proto detection doesn't happen.CPython versions tested on:
3.13
Operating systems tested on:
macOS, Linux
Linked PRs