From 97ae66ccabd0e2eae5a7880ac93bd2ac26bb355e Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 21 Jun 2018 08:13:07 -0700 Subject: [PATCH] 404s ending in slash redirect to remove that slash, closes #309 --- datasette/app.py | 10 +++++++++- tests/test_html.py | 10 ++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/datasette/app.py b/datasette/app.py index fb389d73..5b99147b 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -468,8 +468,16 @@ class Datasette: RowView.as_view(self), "///", ) - self.register_custom_units() + # On 404 with a trailing slash redirect to path without that slash: + @app.middleware("response") + def redirect_on_404_with_trailing_slash(request, original_response): + print("redirect_on_404_with_trailing_slash", request, original_response) + if original_response.status == 404 and request.path.endswith("/"): + path = request.path.rstrip("/") + if request.query_string: + path = "{}?{}".format(path, request.query_string) + return response.redirect(path) @app.exception(Exception) def on_exception(request, exception): diff --git a/tests/test_html.py b/tests/test_html.py index 5eaf2b6e..c3429298 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -697,3 +697,13 @@ def inner_html(soup): # This includes the parent tag - so remove that inner_html = html.split('>', 1)[1].rsplit('<', 1)[0] return inner_html.strip() + + +@pytest.mark.parametrize('path,expected_redirect', [ + ('/fixtures/', '/fixtures'), + ('/fixtures/simple_view/', '/fixtures/simple_view'), +]) +def test_404_trailing_slash_redirect(app_client, path, expected_redirect): + response = app_client.get(path, allow_redirects=False) + assert 302 == response.status + assert expected_redirect == response.headers["Location"]