From af60bd4dbeb2562503d29ae31dbd6f8e1712d549 Mon Sep 17 00:00:00 2001 From: majiayu000 <1835304752@qq.com> Date: Thu, 11 Dec 2025 14:05:37 +0800 Subject: [PATCH] Return 405 Method Not Allowed for non-GET HTTP requests Instead of raising an exception when receiving HEAD, POST, or other non-GET HTTP methods, the server now properly returns a 405 Method Not Allowed response with an Allow: GET header. This change: - Adds InvalidMethod exception for unsupported HTTP methods - Modifies Request.parse() to raise InvalidMethod instead of ValueError - Handles InvalidMethod in ServerProtocol.parse() to return 405 response - Updates tests accordingly Fixes #1677 --- src/websockets/exceptions.py | 15 +++++++++ src/websockets/http11.py | 4 +-- src/websockets/server.py | 14 ++++++++ tests/test_http11.py | 6 ++-- tests/test_server.py | 65 ++++++++++++++++++++++++++++++++++++ 5 files changed, 99 insertions(+), 5 deletions(-) diff --git a/src/websockets/exceptions.py b/src/websockets/exceptions.py index a88deaa66..7918d30f4 100644 --- a/src/websockets/exceptions.py +++ b/src/websockets/exceptions.py @@ -12,6 +12,7 @@ * :exc:`ProxyError` * :exc:`InvalidProxyMessage` * :exc:`InvalidProxyStatus` + * :exc:`InvalidMethod` * :exc:`InvalidMessage` * :exc:`InvalidStatus` * :exc:`InvalidStatusCode` (legacy) @@ -52,6 +53,7 @@ "ProxyError", "InvalidProxyMessage", "InvalidProxyStatus", + "InvalidMethod", "InvalidMessage", "InvalidStatus", "InvalidHeader", @@ -235,6 +237,19 @@ def __str__(self) -> str: return f"proxy rejected connection: HTTP {self.response.status_code:d}" +class InvalidMethod(InvalidHandshake): + """ + Raised when the HTTP method isn't GET. + + """ + + def __init__(self, method: str) -> None: + self.method = method + + def __str__(self) -> str: + return f"invalid HTTP method; expected GET; got {self.method}" + + class InvalidMessage(InvalidHandshake): """ Raised when a handshake request or response is malformed. diff --git a/src/websockets/http11.py b/src/websockets/http11.py index 5af73eb0c..4f9f912e1 100644 --- a/src/websockets/http11.py +++ b/src/websockets/http11.py @@ -9,7 +9,7 @@ from typing import Callable from .datastructures import Headers -from .exceptions import SecurityError +from .exceptions import InvalidMethod, SecurityError from .version import version as websockets_version @@ -148,7 +148,7 @@ def parse( f"unsupported protocol; expected HTTP/1.1: {d(request_line)}" ) if method != b"GET": - raise ValueError(f"unsupported HTTP method; expected GET; got {d(method)}") + raise InvalidMethod(d(method)) path = raw_path.decode("ascii", "surrogateescape") headers = yield from parse_headers(read_line) diff --git a/src/websockets/server.py b/src/websockets/server.py index de2c63548..91242d3f5 100644 --- a/src/websockets/server.py +++ b/src/websockets/server.py @@ -15,6 +15,7 @@ InvalidHeader, InvalidHeaderValue, InvalidMessage, + InvalidMethod, InvalidOrigin, InvalidUpgrade, NegotiationError, @@ -547,6 +548,18 @@ def parse(self) -> Generator[None]: request = yield from Request.parse( self.reader.read_line, ) + except InvalidMethod as exc: + self.handshake_exc = exc + response = self.reject( + http.HTTPStatus.METHOD_NOT_ALLOWED, + f"Failed to open a WebSocket connection: {exc}.\n", + ) + response.headers["Allow"] = "GET" + self.send_response(response) + self.parser = self.discard() + next(self.parser) # start coroutine + yield + return except Exception as exc: self.handshake_exc = InvalidMessage( "did not receive a valid HTTP request" @@ -556,6 +569,7 @@ def parse(self) -> Generator[None]: self.parser = self.discard() next(self.parser) # start coroutine yield + return if self.debug: self.logger.debug("< GET %s HTTP/1.1", request.path) diff --git a/tests/test_http11.py b/tests/test_http11.py index 3328b3b5e..048aebd70 100644 --- a/tests/test_http11.py +++ b/tests/test_http11.py @@ -1,5 +1,5 @@ from websockets.datastructures import Headers -from websockets.exceptions import SecurityError +from websockets.exceptions import InvalidMethod, SecurityError from websockets.http11 import * from websockets.http11 import parse_headers from websockets.streams import StreamReader @@ -61,11 +61,11 @@ def test_parse_unsupported_protocol(self): def test_parse_unsupported_method(self): self.reader.feed_data(b"OPTIONS * HTTP/1.1\r\n\r\n") - with self.assertRaises(ValueError) as raised: + with self.assertRaises(InvalidMethod) as raised: next(self.parse()) self.assertEqual( str(raised.exception), - "unsupported HTTP method; expected GET; got OPTIONS", + "invalid HTTP method; expected GET; got OPTIONS", ) def test_parse_invalid_header(self): diff --git a/tests/test_server.py b/tests/test_server.py index 43970a7cd..31aa10402 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -9,6 +9,7 @@ from websockets.exceptions import ( InvalidHeader, InvalidMessage, + InvalidMethod, InvalidOrigin, InvalidUpgrade, NegotiationError, @@ -257,6 +258,70 @@ def test_receive_junk_request(self): "invalid HTTP request line: HELO relay.invalid", ) + @patch("email.utils.formatdate", return_value=DATE) + def test_receive_head_request(self, _formatdate): + """Server receives a HEAD request and returns 405 Method Not Allowed.""" + server = ServerProtocol() + server.receive_data( + ( + f"HEAD /test HTTP/1.1\r\n" + f"Host: example.com\r\n" + f"\r\n" + ).encode(), + ) + + self.assertEqual(server.events_received(), []) + self.assertIsInstance(server.handshake_exc, InvalidMethod) + self.assertEqual(str(server.handshake_exc), "invalid HTTP method; expected GET; got HEAD") + self.assertEqual( + server.data_to_send(), + [ + f"HTTP/1.1 405 Method Not Allowed\r\n" + f"Date: {DATE}\r\n" + f"Connection: close\r\n" + f"Content-Length: 84\r\n" + f"Content-Type: text/plain; charset=utf-8\r\n" + f"Allow: GET\r\n" + f"\r\n" + f"Failed to open a WebSocket connection: " + f"invalid HTTP method; expected GET; got HEAD.\n".encode(), + b"", + ], + ) + self.assertTrue(server.close_expected()) + + @patch("email.utils.formatdate", return_value=DATE) + def test_receive_post_request(self, _formatdate): + """Server receives a POST request and returns 405 Method Not Allowed.""" + server = ServerProtocol() + server.receive_data( + ( + f"POST /test HTTP/1.1\r\n" + f"Host: example.com\r\n" + f"\r\n" + ).encode(), + ) + + self.assertEqual(server.events_received(), []) + self.assertIsInstance(server.handshake_exc, InvalidMethod) + self.assertEqual(str(server.handshake_exc), "invalid HTTP method; expected GET; got POST") + self.assertEqual( + server.data_to_send(), + [ + f"HTTP/1.1 405 Method Not Allowed\r\n" + f"Date: {DATE}\r\n" + f"Connection: close\r\n" + f"Content-Length: 84\r\n" + f"Content-Type: text/plain; charset=utf-8\r\n" + f"Allow: GET\r\n" + f"\r\n" + f"Failed to open a WebSocket connection: " + f"invalid HTTP method; expected GET; got POST.\n".encode(), + b"", + ], + ) + self.assertTrue(server.close_expected()) + class ResponseTests(unittest.TestCase): """Test generating opening handshake responses."""