Skip to content

Commit 438cb40

Browse files
Tokenize Connection header values in Python HTTP parser (aio-libs#12249)
1 parent 6f059db commit 438cb40

3 files changed

Lines changed: 71 additions & 7 deletions

File tree

CHANGES/12249.bugfix.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Aligned the pure-Python HTTP request parser with the C parser by splitting
2+
comma-separated and repeated ``Connection`` header values for keep-alive,
3+
close, and upgrade handling -- by :user:`rodrigobnogueira`.

aiohttp/http_parser.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -520,16 +520,24 @@ def parse_headers(
520520
if bad_hdr is not None:
521521
raise BadHttpMessage(f"Duplicate '{bad_hdr}' header found.")
522522

523-
# keep-alive
524-
conn = headers.get(hdrs.CONNECTION)
525-
if conn:
526-
v = conn.lower()
527-
if v == "close":
523+
# keep-alive and protocol switching
524+
# RFC 9110 section 7.6.1 defines Connection as a comma-separated list.
525+
conn_values = headers.getall(hdrs.CONNECTION, ())
526+
if conn_values:
527+
conn_tokens = {
528+
token.lower()
529+
for conn_value in conn_values
530+
for token in (part.strip(" \t") for part in conn_value.split(","))
531+
if token and token.isascii()
532+
}
533+
534+
if "close" in conn_tokens:
528535
close_conn = True
529-
elif v == "keep-alive":
536+
elif "keep-alive" in conn_tokens:
530537
close_conn = False
538+
531539
# https://www.rfc-editor.org/rfc/rfc9110.html#name-101-switching-protocols
532-
elif v == "upgrade" and headers.get(hdrs.UPGRADE):
540+
if "upgrade" in conn_tokens and headers.get(hdrs.UPGRADE):
533541
upgrade = True
534542

535543
# encoding

tests/test_http_parser.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,24 @@ def test_conn_keep_alive_1_1(parser: HttpRequestParser) -> None:
509509
assert not msg.should_close
510510

511511

512+
def test_conn_close_comma_list(parser: HttpRequestParser) -> None:
513+
text = b"GET /test HTTP/1.1\r\nconnection: close, keep-alive\r\n\r\n"
514+
messages, upgrade, tail = parser.feed_data(text)
515+
msg = messages[0][0]
516+
assert msg.should_close
517+
518+
519+
def test_conn_close_multiple_headers(parser: HttpRequestParser) -> None:
520+
text = (
521+
b"GET /test HTTP/1.1\r\n"
522+
b"connection: keep-alive\r\n"
523+
b"connection: close\r\n\r\n"
524+
)
525+
messages, upgrade, tail = parser.feed_data(text)
526+
msg = messages[0][0]
527+
assert msg.should_close
528+
529+
512530
def test_conn_other_1_0(parser: HttpRequestParser) -> None:
513531
text = b"GET /test HTTP/1.0\r\nconnection: test\r\n\r\n"
514532
messages, upgrade, tail = parser.feed_data(text)
@@ -609,6 +627,33 @@ def test_conn_upgrade(parser: HttpRequestParser) -> None:
609627
assert upgrade
610628

611629

630+
def test_conn_upgrade_comma_list(parser: HttpRequestParser) -> None:
631+
text = (
632+
b"GET /test HTTP/1.1\r\n"
633+
b"connection: keep-alive, upgrade\r\n"
634+
b"upgrade: websocket\r\n\r\n"
635+
)
636+
messages, upgrade, tail = parser.feed_data(text)
637+
msg = messages[0][0]
638+
assert not msg.should_close
639+
assert msg.upgrade
640+
assert upgrade
641+
642+
643+
def test_conn_upgrade_multiple_headers(parser: HttpRequestParser) -> None:
644+
text = (
645+
b"GET /test HTTP/1.1\r\n"
646+
b"connection: keep-alive\r\n"
647+
b"connection: upgrade\r\n"
648+
b"upgrade: websocket\r\n\r\n"
649+
)
650+
messages, upgrade, tail = parser.feed_data(text)
651+
msg = messages[0][0]
652+
assert not msg.should_close
653+
assert msg.upgrade
654+
assert upgrade
655+
656+
612657
def test_bad_upgrade(parser: HttpRequestParser) -> None:
613658
"""Test not upgraded if missing Upgrade header."""
614659
text = b"GET /test HTTP/1.1\r\nconnection: upgrade\r\n\r\n"
@@ -984,6 +1029,14 @@ def test_http_request_message_after_close(parser: HttpRequestParser) -> None:
9841029
parser.feed_data(text)
9851030

9861031

1032+
def test_http_request_message_after_close_comma_list(parser: HttpRequestParser) -> None:
1033+
text = b"GET / HTTP/1.1\r\nConnection: close, keep-alive\r\n\r\nInvalid\r\n\r\n"
1034+
with pytest.raises(
1035+
http_exceptions.BadHttpMessage, match="Data after `Connection: close`"
1036+
):
1037+
parser.feed_data(text)
1038+
1039+
9871040
def test_http_request_upgrade(parser: HttpRequestParser) -> None:
9881041
text = (
9891042
b"GET /test HTTP/1.1\r\n"

0 commit comments

Comments
 (0)