Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 6812509

Browse files
authored
Implement handling of HTTP HEAD requests. (#7999)
1 parent 2a89ce8 commit 6812509

File tree

3 files changed

+54
-8
lines changed

3 files changed

+54
-8
lines changed

changelog.d/7999.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a long standing bug where HTTP HEAD requests resulted in a 400 error.

synapse/http/server.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -242,10 +242,12 @@ async def _async_render(self, request: Request):
242242
no appropriate method exists. Can be overriden in sub classes for
243243
different routing.
244244
"""
245+
# Treat HEAD requests as GET requests.
246+
request_method = request.method.decode("ascii")
247+
if request_method == "HEAD":
248+
request_method = "GET"
245249

246-
method_handler = getattr(
247-
self, "_async_render_%s" % (request.method.decode("ascii"),), None
248-
)
250+
method_handler = getattr(self, "_async_render_%s" % (request_method,), None)
249251
if method_handler:
250252
raw_callback_return = method_handler(request)
251253

@@ -362,11 +364,15 @@ def _get_handler_for_request(
362364
A tuple of the callback to use, the name of the servlet, and the
363365
key word arguments to pass to the callback
364366
"""
367+
# Treat HEAD requests as GET requests.
365368
request_path = request.path.decode("ascii")
369+
request_method = request.method
370+
if request_method == b"HEAD":
371+
request_method = b"GET"
366372

367373
# Loop through all the registered callbacks to check if the method
368374
# and path regex match
369-
for path_entry in self.path_regexs.get(request.method, []):
375+
for path_entry in self.path_regexs.get(request_method, []):
370376
m = path_entry.pattern.match(request_path)
371377
if m:
372378
# We found a match!
@@ -579,7 +585,7 @@ def set_cors_headers(request: Request):
579585
"""
580586
request.setHeader(b"Access-Control-Allow-Origin", b"*")
581587
request.setHeader(
582-
b"Access-Control-Allow-Methods", b"GET, POST, PUT, DELETE, OPTIONS"
588+
b"Access-Control-Allow-Methods", b"GET, HEAD, POST, PUT, DELETE, OPTIONS"
583589
)
584590
request.setHeader(
585591
b"Access-Control-Allow-Headers",

tests/test_server.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,29 @@ def _callback(request, **kwargs):
157157
self.assertEqual(channel.json_body["error"], "Unrecognized request")
158158
self.assertEqual(channel.json_body["errcode"], "M_UNRECOGNIZED")
159159

160+
def test_head_request(self):
161+
"""
162+
JsonResource.handler_for_request gives correctly decoded URL args to
163+
the callback, while Twisted will give the raw bytes of URL query
164+
arguments.
165+
"""
166+
167+
def _callback(request, **kwargs):
168+
return 200, {"result": True}
169+
170+
res = JsonResource(self.homeserver)
171+
res.register_paths(
172+
"GET", [re.compile("^/_matrix/foo$")], _callback, "test_servlet",
173+
)
174+
175+
# The path was registered as GET, but this is a HEAD request.
176+
request, channel = make_request(self.reactor, b"HEAD", b"/_matrix/foo")
177+
render(request, res, self.reactor)
178+
179+
self.assertEqual(channel.result["code"], b"200")
180+
self.assertNotIn("body", channel.result)
181+
self.assertEqual(channel.headers.getRawHeaders(b"Content-Length"), [b"15"])
182+
160183

161184
class OptionsResourceTests(unittest.TestCase):
162185
def setUp(self):
@@ -255,7 +278,7 @@ def setUp(self):
255278
self.reactor = ThreadedMemoryReactorClock()
256279

257280
def test_good_response(self):
258-
def callback(request):
281+
async def callback(request):
259282
request.write(b"response")
260283
request.finish()
261284

@@ -275,7 +298,7 @@ def test_redirect_exception(self):
275298
with the right location.
276299
"""
277300

278-
def callback(request, **kwargs):
301+
async def callback(request, **kwargs):
279302
raise RedirectException(b"/look/an/eagle", 301)
280303

281304
res = WrapHtmlRequestHandlerTests.TestResource()
@@ -295,7 +318,7 @@ def test_redirect_exception_with_cookie(self):
295318
returned too
296319
"""
297320

298-
def callback(request, **kwargs):
321+
async def callback(request, **kwargs):
299322
e = RedirectException(b"/no/over/there", 304)
300323
e.cookies.append(b"session=yespls")
301324
raise e
@@ -312,3 +335,19 @@ def callback(request, **kwargs):
312335
self.assertEqual(location_headers, [b"/no/over/there"])
313336
cookies_headers = [v for k, v in headers if k == b"Set-Cookie"]
314337
self.assertEqual(cookies_headers, [b"session=yespls"])
338+
339+
def test_head_request(self):
340+
"""A head request should work by being turned into a GET request."""
341+
342+
async def callback(request):
343+
request.write(b"response")
344+
request.finish()
345+
346+
res = WrapHtmlRequestHandlerTests.TestResource()
347+
res.callback = callback
348+
349+
request, channel = make_request(self.reactor, b"HEAD", b"/path")
350+
render(request, res, self.reactor)
351+
352+
self.assertEqual(channel.result["code"], b"200")
353+
self.assertNotIn("body", channel.result)

0 commit comments

Comments
 (0)