From e3f972225adb1d84b80dba132f520cc24cb84229 Mon Sep 17 00:00:00 2001
From: Marcelo Trylesinski <marcelotryle@gmail.com>
Date: Sat, 23 May 2026 17:43:29 +0200
Subject: [PATCH] Only dispatch standard HTTP verbs in `HTTPEndpoint` (#3286)

---
 starlette/endpoints.py  |  6 +++++-
 tests/test_endpoints.py | 17 +++++++++++++++++
 2 files changed, 22 insertions(+), 1 deletion(-)

Index: starlette-0.41.3/starlette/endpoints.py
===================================================================
--- starlette-0.41.3.orig/starlette/endpoints.py
+++ starlette-0.41.3/starlette/endpoints.py
@@ -32,7 +32,11 @@ class HTTPEndpoint:
         request = Request(self.scope, receive=self.receive)
         handler_name = "get" if request.method == "HEAD" and not hasattr(self, "head") else request.method.lower()
 
-        handler: typing.Callable[[Request], typing.Any] = getattr(self, handler_name, self.method_not_allowed)
+        handler: Callable[[Request], Any]
+        if request.method in self._allowed_methods or (request.method == "HEAD" and "GET" in self._allowed_methods):
+            handler = getattr(self, handler_name)
+        else:
+            handler = self.method_not_allowed
         is_async = is_async_callable(handler)
         if is_async:
             response = await handler(request)
Index: starlette-0.41.3/tests/test_endpoints.py
===================================================================
--- starlette-0.41.3.orig/tests/test_endpoints.py
+++ starlette-0.41.3/tests/test_endpoints.py
@@ -47,6 +47,23 @@ def test_http_endpoint_route_method(clie
     assert response.headers["allow"] == "GET"
 
 
+def test_http_endpoint_does_not_dispatch_non_verb_method(test_client_factory: TestClientFactory) -> None:
+    class Endpoint(HTTPEndpoint):
+        async def get(self, request: Request) -> PlainTextResponse:
+            return PlainTextResponse("Hello, world!")  # pragma: no cover
+
+        async def _do_delete(self, request: Request) -> PlainTextResponse:
+            return PlainTextResponse("Privileged helper")  # pragma: no cover
+
+    app = Router(routes=[Route("/", endpoint=Endpoint)])
+    client = test_client_factory(app)
+
+    response = client.request("_DO_DELETE", "/")
+    assert response.status_code == 405
+    assert response.text == "Method Not Allowed"
+    assert response.headers["allow"] == "GET"
+
+
 def test_websocket_endpoint_on_connect(test_client_factory: TestClientFactory) -> None:
     class WebSocketApp(WebSocketEndpoint):
         async def on_connect(self, websocket: WebSocket) -> None:
