Repository: developmentseed/cogeo-mosaic

Issue:
STAC backend error when API response does not contain "matched" or "found" key in `context` object
The `/search` response from stac-fastapi-pgstac application does not by default include a `matched` or `found` key in the `context` object. This is from my observations of  our own deployment, but is replicated with latest stac-fastapi-pgstac e.g.

```bash
$ git clone https://github.com/stac-utils/stac-fastapi-pgstac.git
$ cd stac-fastapi-pgstac
$ docker compose up -d  # includes demo joblin data
$ curl -X POST http://localhost:8082/search --header "Content-Type: application/json" --data "{\"limit\": 1}" | jq .context
{
  "limit": 1,
  "returned": 1
}
```

this causes a problem [here](https://github.com/developmentseed/cogeo-mosaic/blob/d38ba691f2ef6b5879adc70814a94ca7e6306eb3/cogeo_mosaic/backends/stac.py#L214) since `matched` is `None` at this point.

Discussion:

Patch:
diff --git a/cogeo_mosaic/backends/stac.py b/cogeo_mosaic/backends/stac.py
index 6404306..cb897d3 100644
--- a/cogeo_mosaic/backends/stac.py
+++ b/cogeo_mosaic/backends/stac.py
@@ -206,22 +206,28 @@ def _fetch(  # noqa: C901
         # {"page": 1, "limit": 1000, "matched": 5671, "returned": 1000}
         # SAT-API META
         # {"page": 4, "limit": 100, "found": 350, "returned": 50}
+        # STAC api `numberMatched` and `numberReturned` fields in ItemCollection
+        # {"numberMatched": 10, "numberReturned": 5, "features": [...]}
+        # otherwise we don't break early
         ctx = results.get("context", results.get("meta"))
-        matched = ctx.get("matched", ctx.get("found"))
+        returned = ctx.get("returned", results.get("numberReturned"))
+        matched = ctx.get("matched", ctx.get("found") or results.get("numberMatched"))
 
         logger.debug(json.dumps(ctx))
+
         # Check if there is more data to fetch
-        if matched <= ctx["returned"]:
-            break
+        if matched is not None and returned is not None:
+            if matched <= returned:
+                break
 
-        # We shouldn't fetch more item than matched
-        if len(features) == matched:
-            break
+            # We shouldn't fetch more item than matched
+            if len(features) == matched:
+                break
 
-        if len(features) > matched:
-            raise MosaicError(
-                "Something weird is going on, please open an issue in https://github.com/developmentseed/cogeo-mosaic"
-            )
+            if len(features) > matched:
+                raise MosaicError(
+                    "Something weird is going on, please open an issue in https://github.com/developmentseed/cogeo-mosaic"
+                )
         page += 1
 
         # https://github.com/radiantearth/stac-api-spec/blob/master/api-spec.md#paging-extension


Test patch:
diff --git a/tests/test_backends.py b/tests/test_backends.py
index 852157e..78c920a 100644
--- a/tests/test_backends.py
+++ b/tests/test_backends.py
@@ -884,6 +884,42 @@ def test_stac_search(post):
         ]
 
     assert len(stac_search("https://a_stac.api/search", {}, max_items=7)) == 7
+    post.reset_mock()
+
+    post.side_effect = [
+        STACMockResponse(
+            {"features": [{"id": "1"}], "context": {"returned": 1, "limit": 1}}
+        ),
+        STACMockResponse(
+            {"features": [{"id": "2"}], "context": {"returned": 1, "limit": 1}}
+        ),
+    ]
+    assert len(stac_search("https://a_stac.api/search", {}, max_items=2, limit=1)) == 2
+    assert post.call_count == 2
+    post.reset_mock()
+    stac_search.cache_clear()
+
+    post.side_effect = [
+        STACMockResponse(
+            {
+                "features": [{"id": "1"}],
+                "numberMatched": 2,
+                "numberReturned": 1,
+                "context": {},
+            }
+        ),
+        STACMockResponse(
+            {
+                "features": [{"id": "2"}],
+                "numberMatched": 2,
+                "numberReturned": 1,
+                "context": {},
+            }
+        ),
+    ]
+    assert len(stac_search("https://a_stac.api/search", {}, max_items=2, limit=1)) == 2
+    assert post.call_count == 2
+    post.reset_mock()
 
 
 def test_stac_accessor():