Skip to content

Commit 9de55ae

Browse files
committed
fix: remove root path prefix when root_path is set on app (#270)
1 parent 9ee1109 commit 9de55ae

File tree

3 files changed

+134
-12
lines changed

3 files changed

+134
-12
lines changed

CHANGES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
## [Unreleased]
44

5+
## [5.0.3] - 2025-07-22
6+
7+
### Fixed
8+
9+
- fix root-path handling when setting via env var or on app instance
10+
511
## [5.0.2] - 2025-04-07
612

713
### Fixed

stac_fastapi/pgstac/models/links.py

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -58,21 +58,10 @@ def url(self):
5858
# - by uvicorn when running with --root-path
5959
# - by FastAPI when running with FastAPI(root_path="...")
6060
#
61-
# When root path is set by uvicorn, request.url.path will have the root path prefix.
62-
# eg. if root path is "/api" and the path is "/collections",
63-
# the request.url.path will be "/api/collections"
64-
#
6561
# We need to remove the root path prefix from the path before
6662
# joining the base_url and path to get the full url to avoid
6763
# having root_path twice in the url
68-
if (
69-
root_path := self.request.scope.get("root_path")
70-
) and not self.request.app.root_path:
71-
# self.request.app.root_path is set by FastAPI when running with FastAPI(root_path="...")
72-
# If self.request.app.root_path is not set but self.request.scope.get("root_path") is set,
73-
# then the root path is set by uvicorn
74-
# So we need to remove the root path prefix from the path before
75-
# joining the base_url and path to get the full url
64+
if root_path := self.request.scope.get("root_path"):
7665
if path.startswith(root_path):
7766
path = path[len(root_path) :]
7867

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import importlib
2+
3+
import pytest
4+
from starlette.testclient import TestClient
5+
6+
from stac_fastapi.pgstac.db import close_db_connection, connect_to_db
7+
8+
BASE_URL = "http://api.acme.com"
9+
ROOT_PATH = "/stac/v1"
10+
11+
12+
@pytest.fixture(scope="function")
13+
async def app_with_root_path(database, monkeypatch):
14+
"""
15+
Provides the global stac_fastapi.pgstac.app.app instance, configured with a
16+
specific ROOT_PATH environment variable and connected to the test database.
17+
"""
18+
19+
monkeypatch.setenv("ROOT_PATH", ROOT_PATH)
20+
monkeypatch.setenv("PGUSER", database.user)
21+
monkeypatch.setenv("PGPASSWORD", database.password)
22+
monkeypatch.setenv("PGHOST", database.host)
23+
monkeypatch.setenv("PGPORT", str(database.port))
24+
monkeypatch.setenv("PGDATABASE", database.dbname)
25+
monkeypatch.setenv("ENABLE_TRANSACTIONS_EXTENSIONS", "TRUE")
26+
27+
# Reload the app module to pick up the new environment variables
28+
import stac_fastapi.pgstac.app
29+
30+
importlib.reload(stac_fastapi.pgstac.app)
31+
32+
from stac_fastapi.pgstac.app import app, with_transactions
33+
34+
# Ensure the app's root_path is configured as expected
35+
assert (
36+
app.root_path == ROOT_PATH
37+
), f"app_with_root_path fixture: app.root_path is '{app.root_path}', expected '{ROOT_PATH}'"
38+
39+
await connect_to_db(app, add_write_connection_pool=with_transactions)
40+
yield app
41+
await close_db_connection(app)
42+
43+
44+
@pytest.fixture(scope="function")
45+
def client_with_root_path(app_with_root_path):
46+
with TestClient(
47+
app_with_root_path,
48+
base_url=BASE_URL,
49+
root_path=ROOT_PATH,
50+
) as c:
51+
yield c
52+
53+
54+
@pytest.fixture(scope="function")
55+
def loaded_client(client_with_root_path, load_test_data):
56+
col = load_test_data("test_collection.json")
57+
resp = client_with_root_path.post(
58+
"/collections",
59+
json=col,
60+
)
61+
assert resp.status_code == 201
62+
item = load_test_data("test_item.json")
63+
resp = client_with_root_path.post(
64+
f"/collections/{col['id']}/items",
65+
json=item,
66+
)
67+
assert resp.status_code == 201
68+
item = load_test_data("test_item2.json")
69+
resp = client_with_root_path.post(
70+
f"/collections/{col['id']}/items",
71+
json=item,
72+
)
73+
assert resp.status_code == 201
74+
yield client_with_root_path
75+
76+
77+
@pytest.mark.parametrize(
78+
"path",
79+
[
80+
"/search?limit=1",
81+
"/collections?limit=1",
82+
"/collections/test-collection/items?limit=1",
83+
],
84+
)
85+
def test_search_links_are_valid(loaded_client, path):
86+
resp = loaded_client.get(path)
87+
assert resp.status_code == 200
88+
response_json = resp.json()
89+
90+
# Ensure all links start with the expected URL prefix and check that
91+
# there is no root_path duplicated in the URL.
92+
failed_links = []
93+
expected_prefix = f"{BASE_URL}{ROOT_PATH}"
94+
95+
for link in response_json.get("links", []):
96+
href = link["href"]
97+
rel = link.get("rel", "unknown")
98+
99+
# Check if link starts with the expected prefix
100+
if not href.startswith(expected_prefix):
101+
failed_links.append(
102+
{
103+
"rel": rel,
104+
"href": href,
105+
"error": f"does not start with expected prefix '{expected_prefix}'",
106+
}
107+
)
108+
continue
109+
110+
# Check for duplicated root path
111+
remainder = href[len(expected_prefix) :]
112+
if remainder.startswith(ROOT_PATH):
113+
failed_links.append(
114+
{
115+
"rel": rel,
116+
"href": href,
117+
"error": f"contains duplicated root path '{ROOT_PATH}'",
118+
}
119+
)
120+
121+
# If there are failed links, create a detailed error report
122+
if failed_links:
123+
error_report = "Link validation failed:\n"
124+
for failed_link in failed_links:
125+
error_report += f" - rel: '{failed_link['rel']}', href: '{failed_link['href']}' - {failed_link['error']}\n"
126+
127+
raise AssertionError(error_report)

0 commit comments

Comments
 (0)