diff --git a/src/websockets/asyncio/server.py b/src/websockets/asyncio/server.py index ef9bd807..0766ee0b 100644 --- a/src/websockets/asyncio/server.py +++ b/src/websockets/asyncio/server.py @@ -11,7 +11,7 @@ from types import TracebackType from typing import Any, Callable, Mapping, cast -from ..exceptions import InvalidHeader +from ..exceptions import InvalidHandshake, InvalidHeader, InvalidMessage from ..extensions.base import ServerExtensionFactory from ..extensions.permessage_deflate import enable_server_permessage_deflate from ..frames import CloseCode @@ -204,7 +204,13 @@ async def handshake( # response that rejects the handshake. if self.protocol.handshake_exc is not None: - raise self.protocol.handshake_exc + # Suppress re-raise for unsupported HTTP methods (HEAD, POST, etc.) + # — the connection is already being closed cleanly by send_eof(). + if not ( + isinstance(self.protocol.handshake_exc, InvalidMessage) + and isinstance(self.protocol.handshake_exc.__cause__, InvalidHandshake) + ): + raise self.protocol.handshake_exc def process_event(self, event: Event) -> None: """ diff --git a/src/websockets/http11.py b/src/websockets/http11.py index 230e5015..422ccd64 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 InvalidHandshake, SecurityError from .version import version as websockets_version @@ -148,7 +148,9 @@ 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 InvalidHandshake( + f"unsupported HTTP method; expected GET; got {d(method)}" + ) # RFC 9110 defers the definition of URIs to RFC 3986, which allows only # a subset of ASCII. Non-ASCII IRIs must be UTF-8 then percent-encoded. diff --git a/src/websockets/server.py b/src/websockets/server.py index de2c6354..5dbd09de 100644 --- a/src/websockets/server.py +++ b/src/websockets/server.py @@ -552,6 +552,18 @@ def parse(self) -> Generator[None]: "did not receive a valid HTTP request" ) self.handshake_exc.__cause__ = exc + if isinstance(exc, InvalidHandshake): + # Send HTTP 405 for unsupported methods — matching + # gorilla/websocket (StatusMethodNotAllowed) and ws + # (405 abortHandshake). send_response() already + # closes the connection for non-101 responses, so + # skip the common handler below. + response = self.reject( + 405, "Only GET is supported.\n" + ) + self.send_response(response) + yield + return self.send_eof() self.parser = self.discard() next(self.parser) # start coroutine diff --git a/src/websockets/sync/server.py b/src/websockets/sync/server.py index ffd82fba..a345358b 100644 --- a/src/websockets/sync/server.py +++ b/src/websockets/sync/server.py @@ -15,7 +15,7 @@ from types import TracebackType from typing import Any, Callable, Mapping, cast -from ..exceptions import InvalidHeader +from ..exceptions import InvalidHandshake, InvalidHeader, InvalidMessage from ..extensions.base import ServerExtensionFactory from ..extensions.permessage_deflate import enable_server_permessage_deflate from ..frames import CloseCode @@ -186,7 +186,13 @@ def handshake( # response that rejects the handshake. if self.protocol.handshake_exc is not None: - raise self.protocol.handshake_exc + # Suppress re-raise for unsupported HTTP methods (HEAD, POST, etc.) + # — the connection is already being closed cleanly by send_eof(). + if not ( + isinstance(self.protocol.handshake_exc, InvalidMessage) + and isinstance(self.protocol.handshake_exc.__cause__, InvalidHandshake) + ): + raise self.protocol.handshake_exc def process_event(self, event: Event) -> None: """ diff --git a/tests/test_http11.py b/tests/test_http11.py index fb6fca34..8ba478c6 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 InvalidHandshake, SecurityError from websockets.http11 import * from websockets.http11 import parse_headers from websockets.streams import StreamReader @@ -61,7 +61,7 @@ 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(InvalidHandshake) as raised: next(self.parse()) self.assertEqual( str(raised.exception),