Skip to content

Commit 13152c3

Browse files
authored
[PR #10055/c11fe96 backport][3.11] Downgrade logging of invalid HTTP methods on first request to debug level (#10064)
1 parent 1b78cae commit 13152c3

File tree

7 files changed

+152
-6
lines changed

7 files changed

+152
-6
lines changed

CHANGES/10055.misc.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Downgraded logging of invalid HTTP method exceptions on the first request to debug level -- by :user:`bdraco`.
2+
3+
HTTP requests starting with an invalid method are relatively common, especially when connected to the public internet, because browsers or other clients may try to speak SSL to a plain-text server or vice-versa. These exceptions can quickly fill the log with noise when nothing is wrong.

aiohttp/_http_parser.pyx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ from aiohttp.helpers import DEBUG, set_exception
2323

2424
from .http_exceptions import (
2525
BadHttpMessage,
26+
BadHttpMethod,
2627
BadStatusLine,
2728
ContentLengthError,
2829
InvalidHeader,
@@ -831,8 +832,9 @@ cdef parser_error_from_errno(cparser.llhttp_t* parser, data, pointer):
831832
cparser.HPE_INVALID_EOF_STATE,
832833
cparser.HPE_INVALID_TRANSFER_ENCODING}:
833834
return BadHttpMessage(err_msg)
835+
elif errno == cparser.HPE_INVALID_METHOD:
836+
return BadHttpMethod(error=err_msg)
834837
elif errno in {cparser.HPE_INVALID_STATUS,
835-
cparser.HPE_INVALID_METHOD,
836838
cparser.HPE_INVALID_VERSION}:
837839
return BadStatusLine(error=err_msg)
838840
elif errno == cparser.HPE_INVALID_URL:

aiohttp/http_exceptions.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,5 +101,12 @@ def __init__(self, line: str = "", error: Optional[str] = None) -> None:
101101
self.line = line
102102

103103

104+
class BadHttpMethod(BadStatusLine):
105+
"""Invalid HTTP method in status line."""
106+
107+
def __init__(self, line: str = "", error: Optional[str] = None) -> None:
108+
super().__init__(line, error or f"Bad HTTP method in status line {line!r}")
109+
110+
104111
class InvalidURLError(BadHttpMessage):
105112
pass

aiohttp/http_parser.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
)
3939
from .http_exceptions import (
4040
BadHttpMessage,
41+
BadHttpMethod,
4142
BadStatusLine,
4243
ContentEncodingError,
4344
ContentLengthError,
@@ -576,7 +577,7 @@ def parse_message(self, lines: List[bytes]) -> RawRequestMessage:
576577
try:
577578
method, path, version = line.split(" ", maxsplit=2)
578579
except ValueError:
579-
raise BadStatusLine(line) from None
580+
raise BadHttpMethod(line) from None
580581

581582
if len(path) > self.max_line_size:
582583
raise LineTooLong(
@@ -585,7 +586,7 @@ def parse_message(self, lines: List[bytes]) -> RawRequestMessage:
585586

586587
# method
587588
if not TOKENRE.fullmatch(method):
588-
raise BadStatusLine(method)
589+
raise BadHttpMethod(method)
589590

590591
# version
591592
match = VERSRE.fullmatch(version)

aiohttp/web_protocol.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
RawRequestMessage,
3636
StreamWriter,
3737
)
38+
from .http_exceptions import BadHttpMethod
3839
from .log import access_logger, server_logger
3940
from .streams import EMPTY_PAYLOAD, StreamReader
4041
from .tcp_helpers import tcp_keepalive
@@ -687,7 +688,18 @@ def handle_error(
687688
Returns HTTP response with specific status code. Logs additional
688689
information. It always closes current connection.
689690
"""
690-
self.log_exception("Error handling request", exc_info=exc)
691+
if (
692+
self._manager
693+
and self._manager.requests_count == 1
694+
and isinstance(exc, BadHttpMethod)
695+
):
696+
# BadHttpMethod is common when a client sends non-HTTP
697+
# or encrypted traffic to an HTTP port. This is expected
698+
# to happen when connected to the public internet so we log
699+
# it at the debug level as to not fill logs with noise.
700+
self.logger.debug("Error handling request", exc_info=exc)
701+
else:
702+
self.log_exception("Error handling request", exc_info=exc)
691703

692704
# some data already got sent, connection is broken
693705
if request.writer.output_size > 0:

tests/test_http_parser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -965,7 +965,7 @@ def test_http_request_parser_two_slashes(parser) -> None:
965965
def test_http_request_parser_bad_method(
966966
parser, rfc9110_5_6_2_token_delim: bytes
967967
) -> None:
968-
with pytest.raises(http_exceptions.BadStatusLine):
968+
with pytest.raises(http_exceptions.BadHttpMethod):
969969
parser.feed_data(rfc9110_5_6_2_token_delim + b'ET" /get HTTP/1.1\r\n\r\n')
970970

971971

tests/test_web_server.py

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import asyncio
22
import socket
33
from contextlib import suppress
4+
from typing import NoReturn
45
from unittest import mock
56

67
import pytest
78

89
from aiohttp import client, web
10+
from aiohttp.http_exceptions import BadHttpMethod, BadStatusLine
11+
from aiohttp.pytest_plugin import AiohttpClient, AiohttpRawServer
912

1013

1114
async def test_simple_server(aiohttp_raw_server, aiohttp_client) -> None:
@@ -56,7 +59,125 @@ async def handler(request):
5659
logger.exception.assert_called_with("Error handling request", exc_info=exc)
5760

5861

59-
async def test_raw_server_handler_timeout(aiohttp_raw_server, aiohttp_client) -> None:
62+
async def test_raw_server_logs_invalid_method_with_loop_debug(
63+
aiohttp_raw_server: AiohttpRawServer,
64+
aiohttp_client: AiohttpClient,
65+
loop: asyncio.AbstractEventLoop,
66+
) -> None:
67+
exc = BadHttpMethod(b"\x16\x03\x03\x01F\x01".decode(), "error")
68+
69+
async def handler(request: web.BaseRequest) -> NoReturn:
70+
raise exc
71+
72+
loop = asyncio.get_event_loop()
73+
loop.set_debug(True)
74+
logger = mock.Mock()
75+
server = await aiohttp_raw_server(handler, logger=logger)
76+
cli = await aiohttp_client(server)
77+
resp = await cli.get("/path/to")
78+
assert resp.status == 500
79+
assert resp.headers["Content-Type"].startswith("text/plain")
80+
81+
txt = await resp.text()
82+
assert "Traceback (most recent call last):\n" in txt
83+
84+
# BadHttpMethod should be logged as debug
85+
# on the first request since the client may
86+
# be probing for TLS/SSL support which is
87+
# expected to fail
88+
logger.debug.assert_called_with("Error handling request", exc_info=exc)
89+
90+
91+
async def test_raw_server_logs_invalid_method_without_loop_debug(
92+
aiohttp_raw_server: AiohttpRawServer,
93+
aiohttp_client: AiohttpClient,
94+
loop: asyncio.AbstractEventLoop,
95+
) -> None:
96+
exc = BadHttpMethod(b"\x16\x03\x03\x01F\x01".decode(), "error")
97+
98+
async def handler(request: web.BaseRequest) -> NoReturn:
99+
raise exc
100+
101+
loop = asyncio.get_event_loop()
102+
loop.set_debug(False)
103+
logger = mock.Mock()
104+
server = await aiohttp_raw_server(handler, logger=logger, debug=False)
105+
cli = await aiohttp_client(server)
106+
resp = await cli.get("/path/to")
107+
assert resp.status == 500
108+
assert resp.headers["Content-Type"].startswith("text/plain")
109+
110+
txt = await resp.text()
111+
assert "Traceback (most recent call last):\n" not in txt
112+
113+
# BadHttpMethod should be logged as debug
114+
# on the first request since the client may
115+
# be probing for TLS/SSL support which is
116+
# expected to fail
117+
logger.debug.assert_called_with("Error handling request", exc_info=exc)
118+
119+
120+
async def test_raw_server_logs_invalid_method_second_request(
121+
aiohttp_raw_server: AiohttpRawServer,
122+
aiohttp_client: AiohttpClient,
123+
loop: asyncio.AbstractEventLoop,
124+
) -> None:
125+
exc = BadHttpMethod(b"\x16\x03\x03\x01F\x01".decode(), "error")
126+
request_count = 0
127+
128+
async def handler(request: web.BaseRequest) -> web.Response:
129+
nonlocal request_count
130+
request_count += 1
131+
if request_count == 2:
132+
raise exc
133+
return web.Response()
134+
135+
loop = asyncio.get_event_loop()
136+
loop.set_debug(False)
137+
logger = mock.Mock()
138+
server = await aiohttp_raw_server(handler, logger=logger)
139+
cli = await aiohttp_client(server)
140+
resp = await cli.get("/path/to")
141+
assert resp.status == 200
142+
resp = await cli.get("/path/to")
143+
assert resp.status == 500
144+
assert resp.headers["Content-Type"].startswith("text/plain")
145+
# BadHttpMethod should be logged as an exception
146+
# if its not the first request since we know
147+
# that the client already was speaking HTTP
148+
logger.exception.assert_called_with("Error handling request", exc_info=exc)
149+
150+
151+
async def test_raw_server_logs_bad_status_line_as_exception(
152+
aiohttp_raw_server: AiohttpRawServer,
153+
aiohttp_client: AiohttpClient,
154+
loop: asyncio.AbstractEventLoop,
155+
) -> None:
156+
exc = BadStatusLine(b"\x16\x03\x03\x01F\x01".decode(), "error")
157+
158+
async def handler(request: web.BaseRequest) -> NoReturn:
159+
raise exc
160+
161+
loop = asyncio.get_event_loop()
162+
loop.set_debug(False)
163+
logger = mock.Mock()
164+
server = await aiohttp_raw_server(handler, logger=logger, debug=False)
165+
cli = await aiohttp_client(server)
166+
resp = await cli.get("/path/to")
167+
assert resp.status == 500
168+
assert resp.headers["Content-Type"].startswith("text/plain")
169+
170+
txt = await resp.text()
171+
assert "Traceback (most recent call last):\n" not in txt
172+
173+
logger.exception.assert_called_with("Error handling request", exc_info=exc)
174+
175+
176+
async def test_raw_server_handler_timeout(
177+
aiohttp_raw_server: AiohttpRawServer, aiohttp_client: AiohttpClient
178+
) -> None:
179+
loop = asyncio.get_event_loop()
180+
loop.set_debug(True)
60181
exc = asyncio.TimeoutError("error")
61182

62183
async def handler(request):

0 commit comments

Comments
 (0)