Fix remaining base_url issues

This commit is contained in:
Simon Willison 2026-05-30 22:46:45 -07:00
commit 1558ab7989
6 changed files with 85 additions and 56 deletions

View file

@ -2870,19 +2870,22 @@ def wrap_view_function(view_fn, datasette):
def permanent_redirect(path, forward_query_string=False, forward_rest=False):
return wrap_view(
lambda request, send: Response.redirect(
def view(request, send):
redirect_path = (
path
+ (request.url_vars["rest"] if forward_rest else "")
+ (
("?" + request.query_string)
if forward_query_string and request.query_string
else ""
),
status=301,
),
datasette=None,
)
)
)
route_path = request.scope.get("route_path")
if route_path and request.path.endswith(route_path):
redirect_path = request.path[: -len(route_path)] + redirect_path
return Response.redirect(redirect_path, status=301)
return wrap_view(view, datasette=None)
_curly_re = re.compile(r"({.*?})")

View file

@ -19,7 +19,7 @@
</p>
<details open style="border: 2px solid #ccc; border-bottom: none; padding: 0.5em">
<summary style="cursor: pointer;">GET</summary>
<form class="core" method="get" id="api-explorer-get" style="margin-top: 0.7em">
<form class="core" method="get" action="{{ urls.path('-/api') }}" id="api-explorer-get" style="margin-top: 0.7em">
<div>
<label for="path">API path:</label>
<input type="text" id="path" name="path" style="width: 60%">
@ -29,7 +29,7 @@
</details>
<details style="border: 2px solid #ccc; padding: 0.5em">
<summary style="cursor: pointer">POST</summary>
<form class="core" method="post" id="api-explorer-post" style="margin-top: 0.7em">
<form class="core" method="post" action="{{ urls.path('-/api') }}" id="api-explorer-post" style="margin-top: 0.7em">
<div>
<label for="path">API path:</label>
<input type="text" id="path" name="path" style="width: 60%">

View file

@ -11,7 +11,7 @@
<header class="hd"><nav>
<p class="crumbs">
<a href="/">home</a>
<a href="{{ base_url }}">home</a>
</p>
<details class="nav-menu details-menu">
<summary><svg aria-labelledby="nav-menu-svg-title" role="img"
@ -22,11 +22,11 @@
</svg></summary>
<div class="nav-menu-inner">
<ul>
<li><a href="/-/databases">Databases</a></li>
<li><a href="/-/plugins">Installed plugins</a></li>
<li><a href="/-/versions">Version info</a></li>
<li><a href="{{ base_url }}-/databases">Databases</a></li>
<li><a href="{{ base_url }}-/plugins">Installed plugins</a></li>
<li><a href="{{ base_url }}-/versions">Version info</a></li>
</ul>
<form class="nav-menu-logout" action="/-/logout" method="post">
<form class="nav-menu-logout" action="{{ base_url }}-/logout" method="post">
<button class="button-as-link">Log out</button>
</form>
</div>
@ -48,9 +48,9 @@
<header class="hd">
<nav>
<p class="crumbs">
<a href="/">home</a> /
<a href="/fixtures">fixtures</a> /
<a href="/fixtures/attraction_characteristic">attraction_characteristic</a>
<a href="{{ base_url }}">home</a> /
<a href="{{ base_url }}fixtures">fixtures</a> /
<a href="{{ base_url }}fixtures/attraction_characteristic">attraction_characteristic</a>
</p>
<div class="actor">
<strong>testuser</strong>
@ -80,16 +80,16 @@
<a href="https://github.com/simonw/datasette">
About Datasette</a>
</p>
<h2 style="padding-left: 10px; border-left: 10px solid #9403e5"><a href="/fixtures">fixtures</a></h2>
<h2 style="padding-left: 10px; border-left: 10px solid #9403e5"><a href="{{ base_url }}fixtures">fixtures</a></h2>
<p>
1,258 rows in 24 tables, 206 rows in 5 hidden tables, 4 views
</p>
<p><a href="/fixtures/compound_three_primary_keys" title="1001 rows">compound_three_primary_keys</a>, <a href="/fixtures/sortable" title="201 rows">sortable</a>, <a href="/fixtures/facetable" title="15 rows">facetable</a>, <a href="/fixtures/roadside_attraction_characteristics" title="5 rows">roadside_attraction_characteristics</a>, <a href="/fixtures/simple_primary_key" title="4 rows">simple_primary_key</a>, <a href="/fixtures">...</a></p>
<h2 style="padding-left: 10px; border-left: 10px solid #8d777f"><a href="/data">data</a></h2>
<p><a href="{{ base_url }}fixtures/compound_three_primary_keys" title="1001 rows">compound_three_primary_keys</a>, <a href="{{ base_url }}fixtures/sortable" title="201 rows">sortable</a>, <a href="{{ base_url }}fixtures/facetable" title="15 rows">facetable</a>, <a href="{{ base_url }}fixtures/roadside_attraction_characteristics" title="5 rows">roadside_attraction_characteristics</a>, <a href="{{ base_url }}fixtures/simple_primary_key" title="4 rows">simple_primary_key</a>, <a href="{{ base_url }}fixtures">...</a></p>
<h2 style="padding-left: 10px; border-left: 10px solid #8d777f"><a href="{{ base_url }}data">data</a></h2>
<p>
6 rows in 2 tables
</p>
<p><a href="/data/names" title="6 rows">names</a>, <a href="/data/foo">foo</a></p>
<p><a href="{{ base_url }}data/names" title="6 rows">names</a>, <a href="{{ base_url }}data/foo">foo</a></p>
</section>
<h2 class="pattern-heading">.bd for /database</h2>
@ -134,7 +134,7 @@
<a href="https://github.com/simonw/datasette">
About Datasette</a>
</p>
<form class="sql" action="/fixtures" method="get">
<form class="sql" action="{{ base_url }}fixtures" method="get">
<h3>Custom SQL query</h3>
<p><textarea id="sql-editor" name="sql">select * from [123_starts_with_digits]</textarea></p>
<p>
@ -143,17 +143,17 @@
</p>
</form>
<div class="db-table">
<h2><a href="/fixtures/123_starts_with_digits">123_starts_with_digits</a></h2>
<h2><a href="{{ base_url }}fixtures/123_starts_with_digits">123_starts_with_digits</a></h2>
<p><em>content</em></p>
<p>0 rows</p>
</div>
<div class="db-table">
<h2><a href="/fixtures/Table+With+Space+In+Name">Table With Space In Name</a></h2>
<h2><a href="{{ base_url }}fixtures/Table+With+Space+In+Name">Table With Space In Name</a></h2>
<p><em>pk, content</em></p>
<p>0 rows</p>
</div>
<div class="db-table">
<h2><a href="/fixtures/attraction_characteristic">attraction_characteristic</a></h2>
<h2><a href="{{ base_url }}fixtures/attraction_characteristic">attraction_characteristic</a></h2>
<p><em>pk, name</em></p>
<p>2 rows</p>
</div>
@ -202,7 +202,7 @@
<h3>3 rows
where characteristic_id = 2
</h3>
<form class="filters" action="/fixtures/roadside_attraction_characteristics" method="get">
<form class="filters" action="{{ base_url }}fixtures/roadside_attraction_characteristics" method="get">
<div class="search-row"><label for="_search">Search:</label><input id="_search" type="search" name="_search" value=""></div>
<div class="filter-row">
<div class="select-wrapper">
@ -290,16 +290,16 @@
<h3>2 extra where clauses</h3>
<ul>
<li><code>planet_int=1</code> [<a href="/fixtures/facetable?_where=state%3D%27CA%27">remove</a>]</li>
<li><code>planet_int=1</code> [<a href="{{ base_url }}fixtures/facetable?_where=state%3D%27CA%27">remove</a>]</li>
<li><code>state='CA'</code> [<a href="/fixtures/facetable?_where=planet_int%3D1">remove</a>]</li>
<li><code>state='CA'</code> [<a href="{{ base_url }}fixtures/facetable?_where=planet_int%3D1">remove</a>]</li>
</ul>
</div>
<p><a class="not-underlined" title="select rowid, attraction_id, characteristic_id from roadside_attraction_characteristics where &#34;characteristic_id&#34; = :p0 order by rowid limit 101" href="/fixtures?sql=select+rowid%2C+attraction_id%2C+characteristic_id+from+roadside_attraction_characteristics+where+%22characteristic_id%22+%3D+%3Ap0+order+by+rowid+limit+101&amp;p0=2">&#x270e; <span class="underlined">View and edit SQL</span></a></p>
<p><a class="not-underlined" title="select rowid, attraction_id, characteristic_id from roadside_attraction_characteristics where &#34;characteristic_id&#34; = :p0 order by rowid limit 101" href="{{ base_url }}fixtures?sql=select+rowid%2C+attraction_id%2C+characteristic_id+from+roadside_attraction_characteristics+where+%22characteristic_id%22+%3D+%3Ap0+order+by+rowid+limit+101&amp;p0=2">&#x270e; <span class="underlined">View and edit SQL</span></a></p>
<p class="export-links">This data as <a href="/fixtures/roadside_attraction_characteristics.json?characteristic_id=2&amp;_labels=on">json</a>, <a href="/fixtures/roadside_attraction_characteristics.csv?characteristic_id=2&amp;_labels=on&amp;_size=max">CSV</a> (<a href="#export">advanced</a>)</p>
<p class="export-links">This data as <a href="{{ base_url }}fixtures/roadside_attraction_characteristics.json?characteristic_id=2&amp;_labels=on">json</a>, <a href="{{ base_url }}fixtures/roadside_attraction_characteristics.csv?characteristic_id=2&amp;_labels=on&amp;_size=max">CSV</a> (<a href="#export">advanced</a>)</p>
<p class="suggested-facets">
Suggested facets: <a href="http://latest.datasette.io/fixtures/facetable?_where=planet_int%3D1&amp;_where=state%3D%27CA%27&amp;_facet=city_id&amp;_facet=created&amp;_facet=complex_array&amp;_facet=tags#facet-tags">tags</a>, <a href="http://latest.datasette.io/fixtures/facetable?_where=planet_int%3D1&amp;_where=state%3D%27CA%27&amp;_facet=city_id&amp;_facet=created&amp;_facet=complex_array&amp;_facet_date=created#facet-created">created</a> (date), <a href="http://latest.datasette.io/fixtures/facetable?_where=planet_int%3D1&amp;_where=state%3D%27CA%27&amp;_facet=city_id&amp;_facet=created&amp;_facet=complex_array&amp;_facet_array=tags#facet-tags">tags</a> (array)
@ -311,7 +311,7 @@
<p class="facet-info-name">
<strong>tags (array)</strong>
<a href="/fixtures/facetable?_where=planet_int%3D1&amp;_where=state%3D%27CA%27&amp;_facet=city_id&amp;_facet=created" class="cross"></a>
<a href="{{ base_url }}fixtures/facetable?_where=planet_int%3D1&amp;_where=state%3D%27CA%27&amp;_facet=city_id&amp;_facet=created" class="cross"></a>
</p>
<ul>
@ -336,7 +336,7 @@
<p class="facet-info-name">
<strong>created</strong>
<a href="/fixtures/facetable?_where=planet_int%3D1&amp;_where=state%3D%27CA%27&amp;_facet=city_id&amp;_facet_array=tags" class="cross"></a>
<a href="{{ base_url }}fixtures/facetable?_where=planet_int%3D1&amp;_where=state%3D%27CA%27&amp;_facet=city_id&amp;_facet_array=tags" class="cross"></a>
</p>
<ul>
@ -361,7 +361,7 @@
<p class="facet-info-name">
<strong>city_id</strong>
<a href="/fixtures/facetable?_where=planet_int%3D1&amp;_where=state%3D%27CA%27&amp;_facet=created&amp;_facet_array=tags" class="cross"></a>
<a href="{{ base_url }}fixtures/facetable?_where=planet_int%3D1&amp;_where=state%3D%27CA%27&amp;_facet=created&amp;_facet_array=tags" class="cross"></a>
</p>
<ul>
@ -387,45 +387,45 @@
Link
</th>
<th class="col-rowid" scope="col">
<a href="/fixtures/roadside_attraction_characteristics?characteristic_id=2&amp;_sort_desc=rowid" rel="nofollow">rowid&nbsp;</a>
<a href="{{ base_url }}fixtures/roadside_attraction_characteristics?characteristic_id=2&amp;_sort_desc=rowid" rel="nofollow">rowid&nbsp;</a>
</th>
<th class="col-attraction_id" scope="col">
<a href="/fixtures/roadside_attraction_characteristics?characteristic_id=2&amp;_sort=attraction_id" rel="nofollow">attraction_id</a>
<a href="{{ base_url }}fixtures/roadside_attraction_characteristics?characteristic_id=2&amp;_sort=attraction_id" rel="nofollow">attraction_id</a>
</th>
<th class="col-characteristic_id" scope="col">
<a href="/fixtures/roadside_attraction_characteristics?characteristic_id=2&amp;_sort=characteristic_id" rel="nofollow">characteristic_id</a>
<a href="{{ base_url }}fixtures/roadside_attraction_characteristics?characteristic_id=2&amp;_sort=characteristic_id" rel="nofollow">characteristic_id</a>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="col-Link"><a href="/fixtures/roadside_attraction_characteristics/1">1</a></td>
<td class="col-Link"><a href="{{ base_url }}fixtures/roadside_attraction_characteristics/1">1</a></td>
<td class="col-rowid">1</td>
<td class="col-attraction_id"><a href="/fixtures/roadside_attractions/1">The Mystery Spot</a>&nbsp;<em>1</em></td>
<td class="col-characteristic_id"><a href="/fixtures/attraction_characteristic/2">Paranormal</a>&nbsp;<em>2</em></td>
<td class="col-attraction_id"><a href="{{ base_url }}fixtures/roadside_attractions/1">The Mystery Spot</a>&nbsp;<em>1</em></td>
<td class="col-characteristic_id"><a href="{{ base_url }}fixtures/attraction_characteristic/2">Paranormal</a>&nbsp;<em>2</em></td>
</tr>
<tr>
<td class="col-Link"><a href="/fixtures/roadside_attraction_characteristics/2">2</a></td>
<td class="col-Link"><a href="{{ base_url }}fixtures/roadside_attraction_characteristics/2">2</a></td>
<td class="col-rowid">2</td>
<td class="col-attraction_id"><a href="/fixtures/roadside_attractions/2">Winchester Mystery House</a>&nbsp;<em>2</em></td>
<td class="col-characteristic_id"><a href="/fixtures/attraction_characteristic/2">Paranormal</a>&nbsp;<em>2</em></td>
<td class="col-attraction_id"><a href="{{ base_url }}fixtures/roadside_attractions/2">Winchester Mystery House</a>&nbsp;<em>2</em></td>
<td class="col-characteristic_id"><a href="{{ base_url }}fixtures/attraction_characteristic/2">Paranormal</a>&nbsp;<em>2</em></td>
</tr>
<tr>
<td class="col-Link"><a href="/fixtures/roadside_attraction_characteristics/3">3</a></td>
<td class="col-Link"><a href="{{ base_url }}fixtures/roadside_attraction_characteristics/3">3</a></td>
<td class="col-rowid">3</td>
<td class="col-attraction_id"><a href="/fixtures/roadside_attractions/4">Bigfoot Discovery Museum</a>&nbsp;<em>4</em></td>
<td class="col-characteristic_id"><a href="/fixtures/attraction_characteristic/2">Paranormal</a>&nbsp;<em>2</em></td>
<td class="col-attraction_id"><a href="{{ base_url }}fixtures/roadside_attractions/4">Bigfoot Discovery Museum</a>&nbsp;<em>4</em></td>
<td class="col-characteristic_id"><a href="{{ base_url }}fixtures/attraction_characteristic/2">Paranormal</a>&nbsp;<em>2</em></td>
</tr>
</tbody>
</table>
<div id="export" class="advanced-export">
<h3>Advanced export</h3>
<p>JSON shape:
<a href="/fixtures/roadside_attraction_characteristics.json?characteristic_id=2&amp;_labels=on">default</a>,
<a href="/fixtures/roadside_attraction_characteristics.json?characteristic_id=2&amp;_labels=on&amp;_shape=array">array</a>,
<a href="/fixtures/roadside_attraction_characteristics.json?characteristic_id=2&amp;_labels=on&amp;_shape=array&amp;_nl=on">newline-delimited</a>
<a href="{{ base_url }}fixtures/roadside_attraction_characteristics.json?characteristic_id=2&amp;_labels=on">default</a>,
<a href="{{ base_url }}fixtures/roadside_attraction_characteristics.json?characteristic_id=2&amp;_labels=on&amp;_shape=array">array</a>,
<a href="{{ base_url }}fixtures/roadside_attraction_characteristics.json?characteristic_id=2&amp;_labels=on&amp;_shape=array&amp;_nl=on">newline-delimited</a>
</p>
<form action="/fixtures/roadside_attraction_characteristics.csv" method="get">
<form action="{{ base_url }}fixtures/roadside_attraction_characteristics.csv" method="get">
<p>
CSV options:
<label><input type="checkbox" name="_dl"> download file</label>
@ -445,7 +445,7 @@
<h2 class="pattern-heading">.bd for /database/table/row</h2>
<section class="content">
<h1 style="padding-left: 10px; border-left: 10px solid #ff0000">roadside_attractions: 2</h1>
<p>This data as <a href="/fixtures/roadside_attractions/2.json">json</a></p>
<p>This data as <a href="{{ base_url }}fixtures/roadside_attractions/2.json">json</a></p>
<table class="rows-and-columns">
<thead>
<tr>
@ -479,7 +479,7 @@
<h2>Links from other tables</h2>
<ul>
<li>
<a href="/fixtures/roadside_attraction_characteristics?attraction_id=2">
<a href="{{ base_url }}fixtures/roadside_attraction_characteristics?attraction_id=2">
1 row</a>
from attraction_id in roadside_attraction_characteristics
</li>

View file

@ -60,9 +60,11 @@ class DatabaseView(View):
sql = (request.args.get("sql") or "").strip()
if sql:
redirect_url = "/" + request.url_vars.get("database") + "/-/query"
redirect_url = datasette.urls.database(database) + "/-/query"
if request.url_vars.get("format"):
redirect_url += "." + request.url_vars.get("format")
redirect_url = path_with_format(
path=redirect_url, format=request.url_vars.get("format")
)
redirect_url += "?" + request.query_string
response = Response.redirect(redirect_url)
if datasette.cors:

View file

@ -892,14 +892,15 @@ class ApiExplorerView(BaseView):
raise Forbidden("You do not have permission to view this instance")
def api_path(link):
return "/-/api#{}".format(
return "{}#{}".format(
self.ds.urls.path("/-/api"),
urllib.parse.urlencode(
{
key: json.dumps(value, indent=2) if key == "json" else value
for key, value in link.items()
if key in ("path", "method", "json")
}
)
),
)
return await self.render(

View file

@ -878,6 +878,8 @@ def test_debug_context_includes_extra_template_vars():
"/fixtures/facetable",
"/fixtures/facetable?_facet=state",
"/fixtures/-/query?sql=select+1",
"/-/api",
"/-/patterns",
],
)
@pytest.mark.parametrize("use_prefix", (True, False))
@ -932,7 +934,9 @@ def test_base_url_config(app_client_base_url_prefix, path, use_prefix):
):
# If this has been made absolute it may start http://localhost/
if href.startswith("http://localhost/"):
href = href[len("http://localost/") :]
href = href[len("http://localhost") :]
elif href.startswith(("http://", "https://")):
continue
assert href.startswith("/prefix/"), json.dumps(
{
"path": path,
@ -966,6 +970,25 @@ def test_base_url_affects_filter_redirects(app_client_base_url_prefix):
)
def test_base_url_affects_database_sql_redirect(app_client_base_url_prefix):
response = app_client_base_url_prefix.get(
"/prefix/fixtures?sql=select+1", follow_redirects=False
)
assert response.status_code == 302
assert response.headers["location"] == "/prefix/fixtures/-/query?sql=select+1"
def test_base_url_affects_permanent_redirects():
with make_app_client(memory=True, settings={"base_url": "/prefix/"}) as client:
response = client.get("/prefix/-", follow_redirects=False)
assert response.status_code == 301
assert response.headers["location"] == "/prefix/-/"
response2 = client.get("/prefix/:memory:", follow_redirects=False)
assert response2.status_code == 301
assert response2.headers["location"] == "/prefix/_memory"
def test_base_url_affects_metadata_extra_css_urls(app_client_base_url_prefix):
html = app_client_base_url_prefix.get("/").text
assert '<link rel="stylesheet" href="/prefix/static/extra-css-urls.css">' in html