diff --git a/datasette/app.py b/datasette/app.py index f86a14b5..22584379 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -36,7 +36,7 @@ from .utils import ( sqlite_timelimit, to_css_class, ) -from .utils.asgi import asgi_static, asgi_send_html, asgi_send_json +from .utils.asgi import asgi_static, asgi_send_html, asgi_send_json, asgi_send_redirect from .tracer import capture_traces, trace from .plugins import pm, DEFAULT_PLUGINS from .version import __version__ @@ -652,6 +652,17 @@ class Datasette: outer_self = self class DatasetteRouter(AsgiRouter): + async def handle_404(self, scope, receive, send): + # If URL has a trailing slash, redirect to URL without it + path = scope.get("raw_path", scope["path"].encode("utf8")) + if path.endswith(b"/"): + path = path.rstrip(b"/") + if scope["query_string"]: + path += b"?" + scope["query_string"] + await asgi_send_redirect(send, path.decode("latin1")) + else: + await super().handle_404(scope, receive, send) + async def handle_500(self, scope, receive, send, exception): title = None help = None @@ -693,17 +704,6 @@ class Datasette: ) app = DatasetteRouter(routes) - # On 404 with a trailing slash redirect to path without that slash: - # pylint: disable=unused-variable - # TODO: re-enable this - # @app.middleware("response") - # def 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) - # First time server starts up, calculate table counts for immutable databases # TODO: re-enable this mechanism # @app.listener("before_server_start") diff --git a/datasette/utils/asgi.py b/datasette/utils/asgi.py index 14ade563..2aeeb836 100644 --- a/datasette/utils/asgi.py +++ b/datasette/utils/asgi.py @@ -1,6 +1,7 @@ import json from mimetypes import guess_type from sanic.views import HTTPMethodView +from sanic.request import Request as SanicRequest from pathlib import Path import re import aiofiles @@ -166,6 +167,16 @@ async def asgi_send_html(send, html, status=200, headers=None): ) +async def asgi_send_redirect(send, location, status=302): + await asgi_send( + send, + "", + status=status, + headers={"Location": location}, + content_type="text/html", + ) + + async def asgi_send(send, content, status, headers, content_type="text/plain"): await asgi_start(send, status, headers, content_type) await send({"type": "http.response.body", "body": content.encode("utf8")}) diff --git a/datasette/views/base.py b/datasette/views/base.py index a2d6571f..4bf251fb 100644 --- a/datasette/views/base.py +++ b/datasette/views/base.py @@ -9,7 +9,6 @@ import jinja2 import pint from sanic import response from sanic.exceptions import NotFound -from sanic.request import Request as SanicRequest from html import escape