Skip to content

Commit 73f8c78

Browse files
Yuri ZmytrakovYuri Zmytrakov
authored andcommitted
fix: the default and max allowed returned values for collections and items
Ensure collections and items endpoints use their respective default and maximum limit values.
1 parent 7065798 commit 73f8c78

File tree

5 files changed

+120
-66
lines changed

5 files changed

+120
-66
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1111

1212
### Changed
1313

14+
- Changed the default and maximum pagination limits for collections/items endpoints. [#482](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/482)
15+
1416
### Fixed
1517

1618
[v6.5.0] - 2025-09-29

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,10 @@ You can customize additional settings in your `.env` file:
299299
| `ENABLE_COLLECTIONS_SEARCH` | Enable collection search extensions (sort, fields, free text search, structured filtering, and datetime filtering) on the core `/collections` endpoint. | `true` | Optional |
300300
| `ENABLE_COLLECTIONS_SEARCH_ROUTE` | Enable the custom `/collections-search` endpoint (both GET and POST methods). When disabled, the custom endpoint will not be available, but collection search extensions will still be available on the core `/collections` endpoint if `ENABLE_COLLECTIONS_SEARCH` is true. | `false` | Optional |
301301
| `ENABLE_TRANSACTIONS_EXTENSIONS` | Enables or disables the Transactions and Bulk Transactions API extensions. This is useful for deployments where mutating the catalog via the API should be prevented. If set to `true`, the POST `/collections` route for search will be unavailable in the API. | `true` | Optional |
302-
| `STAC_ITEM_LIMIT` | Sets the environment variable for result limiting to SFEOS for the number of returned items and STAC collections. | `10` | Optional |
302+
| `STAC_GLOBAL_COLLECTION_MAX_LIMIT` | Configures the maximum number of STAC collections that can be returned in a single search request. | `300` | Optional |
303+
| `STAC_DEFAULT_COLLECTION_LIMIT` | Configures the default number of STAC collections returned when no limit parameter is specified in the request. | `100` | Optional |
304+
| `STAC_GLOBAL_ITEM_MAX_LIMIT` | Configures the maximum number of STAC items that can be returned in a single search request. | `100` | Optional |
305+
| `STAC_DEFAULT_ITEM_LIMIT` | Configures the default number of STAC items returned when no limit parameter is specified in the request. | `10` | Optional |
303306
| `STAC_INDEX_ASSETS` | Controls if Assets are indexed when added to Elasticsearch/Opensearch. This allows asset fields to be included in search queries. | `false` | Optional |
304307
| `ENV_MAX_LIMIT` | Configures the environment variable in SFEOS to override the default `MAX_LIMIT`, which controls the limit parameter for returned items and STAC collections. | `10,000` | Optional |
305308
| `USE_DATETIME` | Configures the datetime search behavior in SFEOS. When enabled, searches both datetime field and falls back to start_datetime/end_datetime range for items with null datetime. When disabled, searches only by start_datetime/end_datetime range. | `true` | Optional |

stac_fastapi/core/stac_fastapi/core/core.py

Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -270,33 +270,27 @@ async def all_collections(
270270
base_url = str(request.base_url)
271271

272272
# Get the global limit from environment variable
273-
global_limit = None
274-
env_limit = os.getenv("STAC_ITEM_LIMIT")
275-
if env_limit:
276-
try:
277-
global_limit = int(env_limit)
278-
except ValueError:
279-
# Handle invalid integer in environment variable
280-
pass
281-
282-
# Apply global limit if it exists
283-
if global_limit is not None:
284-
# If a limit was provided, use the smaller of the two
285-
if limit is not None:
286-
limit = min(limit, global_limit)
287-
else:
288-
limit = global_limit
273+
global_max_limit = int(os.getenv("STAC_GLOBAL_COLLECTION_MAX_LIMIT", 300))
274+
default_limit = int(os.getenv("STAC_DEFAULT_COLLECTION_LIMIT", 100))
275+
query_limit = request.query_params.get("limit")
276+
277+
body_limit = None
278+
try:
279+
if request.method == "POST" and request.body():
280+
body_data = await request.json()
281+
body_limit = body_data.get("limit")
282+
except Exception:
283+
pass
284+
285+
if body_limit is not None:
286+
limit = int(body_limit)
287+
elif query_limit:
288+
limit = int(query_limit)
289289
else:
290-
# No global limit, use provided limit or default
291-
if limit is None:
292-
query_limit = request.query_params.get("limit")
293-
if query_limit:
294-
try:
295-
limit = int(query_limit)
296-
except ValueError:
297-
limit = 10
298-
else:
299-
limit = 10
290+
limit = default_limit
291+
292+
if global_max_limit > 0:
293+
limit = min(limit, global_max_limit)
300294

301295
token = request.query_params.get("token")
302296

@@ -562,7 +556,7 @@ async def item_collection(
562556
request (Request): FastAPI Request object.
563557
bbox (Optional[BBox]): Optional bounding box filter.
564558
datetime (Optional[str]): Optional datetime or interval filter.
565-
limit (Optional[int]): Optional page size. Defaults to env ``STAC_ITEM_LIMIT`` when unset.
559+
limit (Optional[int]): Optional page size. Defaults to env `STAC_DEFAULT_ITEM_LIMIT` when unset.
566560
sortby (Optional[str]): Optional sort specification. Accepts repeated values
567561
like ``sortby=-properties.datetime`` or ``sortby=+id``. Bare fields (e.g. ``sortby=id``)
568562
imply ascending order.
@@ -660,7 +654,17 @@ async def get_search(
660654
Raises:
661655
HTTPException: If any error occurs while searching the catalog.
662656
"""
663-
limit = int(request.query_params.get("limit", os.getenv("STAC_ITEM_LIMIT", 10)))
657+
global_max_limit = int(os.getenv("STAC_GLOBAL_ITEM_MAX_LIMIT", 100))
658+
default_limit = int(os.getenv("STAC_DEFAULT_ITEM_LIMIT", 10))
659+
660+
query_limit = request.query_params.get("limit")
661+
if query_limit:
662+
limit = int(query_limit)
663+
else:
664+
limit = default_limit
665+
666+
if global_max_limit > 0:
667+
limit = min(limit, global_max_limit)
664668

665669
base_args = {
666670
"collections": collections,
@@ -736,6 +740,21 @@ async def post_search(
736740
Raises:
737741
HTTPException: If there is an error with the cql2_json filter.
738742
"""
743+
global_max_limit = int(os.getenv("STAC_GLOBAL_ITEM_MAX_LIMIT", 100))
744+
default_limit = int(os.getenv("STAC_DEFAULT_ITEM_LIMIT", 10))
745+
746+
requested_limit = getattr(search_request, "limit", None)
747+
748+
if requested_limit is None:
749+
limit = default_limit
750+
else:
751+
limit = requested_limit
752+
753+
if global_max_limit > 0:
754+
limit = min(limit, global_max_limit)
755+
756+
search_request.limit = limit
757+
739758
base_url = str(request.base_url)
740759

741760
search = self.database.make_search()
@@ -812,10 +831,6 @@ async def post_search(
812831
if hasattr(search_request, "sortby") and getattr(search_request, "sortby"):
813832
sort = self.database.populate_sort(getattr(search_request, "sortby"))
814833

815-
limit = 10
816-
if search_request.limit:
817-
limit = search_request.limit
818-
819834
# Use token from the request if the model doesn't define it
820835
token_param = getattr(
821836
search_request, "token", None

stac_fastapi/tests/api/test_api.py

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1475,47 +1475,41 @@ def create_items(date_prefix: str, start_day: int, count: int) -> dict:
14751475

14761476

14771477
@pytest.mark.asyncio
1478-
async def test_collections_limit_env_variable(app_client, txn_client, load_test_data):
1479-
limit = "5"
1480-
os.environ["STAC_ITEM_LIMIT"] = limit
1481-
item = load_test_data("test_collection.json")
1478+
async def test_global_collection_max_limit_set(app_client, txn_client, load_test_data):
1479+
"""Test with global collection max limit set, expect cap the limit"""
1480+
os.environ["STAC_GLOBAL_COLLECTION_MAX_LIMIT"] = "5"
14821481

14831482
for i in range(10):
1484-
test_collection = item.copy()
1485-
test_collection["id"] = f"test-collection-env-{i}"
1486-
test_collection["title"] = f"Test Collection Env {i}"
1483+
test_collection = load_test_data("test_collection.json")
1484+
test_collection_id = f"test-collection-global-{i}"
1485+
test_collection["id"] = test_collection_id
14871486
await create_collection(txn_client, test_collection)
14881487

1489-
resp = await app_client.get("/collections")
1488+
resp = await app_client.get("/collections?limit=10")
14901489
assert resp.status_code == 200
14911490
resp_json = resp.json()
1492-
assert int(limit) == len(resp_json["collections"])
1491+
assert len(resp_json["collections"]) == 5
14931492

1493+
del os.environ["STAC_GLOBAL_COLLECTION_MAX_LIMIT"]
14941494

1495-
@pytest.mark.asyncio
1496-
async def test_search_collection_limit_env_variable(
1497-
app_client, txn_client, load_test_data
1498-
):
1499-
limit = "5"
1500-
os.environ["STAC_ITEM_LIMIT"] = limit
15011495

1502-
test_collection = load_test_data("test_collection.json")
1503-
test_collection_id = "test-collection-search-limit"
1504-
test_collection["id"] = test_collection_id
1505-
await create_collection(txn_client, test_collection)
1506-
1507-
item = load_test_data("test_item.json")
1508-
item["collection"] = test_collection_id
1496+
@pytest.mark.asyncio
1497+
async def test_default_collection_limit(app_client, txn_client, load_test_data):
1498+
"""Test default collection limit set, should use default when no limit provided"""
1499+
os.environ["STAC_DEFAULT_COLLECTION_LIMIT"] = "5"
15091500

15101501
for i in range(10):
1511-
test_item = item.copy()
1512-
test_item["id"] = f"test-item-search-{i}"
1513-
await create_item(txn_client, test_item)
1502+
test_collection = load_test_data("test_collection.json")
1503+
test_collection_id = f"test-collection-default-{i}"
1504+
test_collection["id"] = test_collection_id
1505+
await create_collection(txn_client, test_collection)
15141506

1515-
resp = await app_client.get("/search", params={"collections": [test_collection_id]})
1507+
resp = await app_client.get("/collections")
15161508
assert resp.status_code == 200
15171509
resp_json = resp.json()
1518-
assert int(limit) == len(resp_json["features"])
1510+
assert len(resp_json["collections"]) == 5
1511+
1512+
del os.environ["STAC_DEFAULT_COLLECTION_LIMIT"]
15191513

15201514

15211515
async def test_search_max_item_limit(

stac_fastapi/tests/api/test_api_item_collection.py

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,47 @@
1010

1111

1212
@pytest.mark.asyncio
13-
async def test_item_collection_limit_env_variable(
13+
async def test_global_item_max_limit_set(app_client, txn_client, load_test_data):
14+
"""Test with global max limit set for items, expect cap the ?limit parameter"""
15+
os.environ["STAC_GLOBAL_ITEM_MAX_LIMIT"] = "5"
16+
17+
test_collection = load_test_data("test_collection.json")
18+
test_collection_id = "test-collection-for-items"
19+
test_collection["id"] = test_collection_id
20+
await create_collection(txn_client, test_collection)
21+
22+
item = load_test_data("test_item.json")
23+
item["collection"] = test_collection_id
24+
25+
for i in range(10):
26+
test_item = item.copy()
27+
test_item["id"] = f"test-item-{i}"
28+
await create_item(txn_client, test_item)
29+
30+
# Test GET /collections/{id}/items with limit parameter
31+
resp = await app_client.get(f"/collections/{test_collection_id}/items?limit=10")
32+
assert resp.status_code == 200
33+
resp_json = resp.json()
34+
assert len(resp_json["features"]) == 5
35+
36+
# Test GET /search with limit parameter
37+
resp = await app_client.get(f"/search?collections={test_collection_id}&limit=10")
38+
assert resp.status_code == 200
39+
resp_json = resp.json()
40+
assert len(resp_json["features"]) == 5
41+
42+
del os.environ["STAC_GLOBAL_ITEM_MAX_LIMIT"]
43+
44+
45+
@pytest.mark.asyncio
46+
async def test_default_item_limit_without_limit_parameter_set(
1447
app_client, txn_client, load_test_data
1548
):
16-
limit = "5"
17-
os.environ["STAC_ITEM_LIMIT"] = limit
49+
"""Test default item limit set, should use default when no limit provided"""
50+
os.environ["STAC_DEFAULT_ITEM_LIMIT"] = "5"
1851

1952
test_collection = load_test_data("test_collection.json")
20-
test_collection_id = "test-collection-items-limit"
53+
test_collection_id = "test-collection-items"
2154
test_collection["id"] = test_collection_id
2255
await create_collection(txn_client, test_collection)
2356

@@ -26,13 +59,20 @@ async def test_item_collection_limit_env_variable(
2659

2760
for i in range(10):
2861
test_item = item.copy()
29-
test_item["id"] = f"test-item-collection-{i}"
62+
test_item["id"] = f"test-item-{i}"
3063
await create_item(txn_client, test_item)
3164

3265
resp = await app_client.get(f"/collections/{test_collection_id}/items")
3366
assert resp.status_code == 200
3467
resp_json = resp.json()
35-
assert int(limit) == len(resp_json["features"])
68+
assert len(resp_json["features"]) == 5
69+
70+
resp = await app_client.get(f"/search?collections={test_collection_id}")
71+
assert resp.status_code == 200
72+
resp_json = resp.json()
73+
assert len(resp_json["features"]) == 5
74+
75+
del os.environ["STAC_DEFAULT_ITEM_LIMIT"]
3676

3777

3878
@pytest.mark.asyncio

0 commit comments

Comments
 (0)