Skip to content

A features value that is not a list is refused - #86

Merged
ChelseaKR merged 1 commit into
mainfrom
fix/a-features-value-that-is-not-a-list
Sep 8, 2026
Merged

ChelseaKR merged 1 commit into
mainfrom
fix/a-features-value-that-is-not-a-list

Conversation

@ChelseaKR

Copy link
Copy Markdown
Owner

What was wrong

iter_features reads payload["features"], yields from it, and steps its offset by its length. Both of those do something plausible to a mapping: yield from over a dict yields its keys, and len() of it counts them. So a page whose features is an object made the walk emit the field names as though they were features and step its offset by the number of them, and nothing raised.

Measured on unmodified origin/main, with _get substituted and a page carrying {"features": {"OBJECTID": 1, "YEAR_": 2020}}:

origin/main yielded: ['OBJECTID', 'YEAR_']
requests made: 1

That is the whole finding, and it is stronger than any sabotage because nothing about it was arranged.

How it was found

wildfire-service-territory-overlap keeps its own paged walk, fetch_feature_pages, which its docs/UPSTREAM.md records as a compensation for this package not exposing an output format. #84 closed that, and moving that project onto iter_features would mean deleting the local walk. Its local walk does one thing this one did not:

page = payload.get("features")
if not isinstance(page, list):
    raise AcquisitionFailed(f"{endpoint} answered with no features array")

A compensation cannot be retired until the thing it compensates for is gone, so keeping that check downstream would leave a fragment of the duplication in place for no reason. The check belongs here.

What changed

iter_features refuses a features value that is not a list, naming the type it got and the offset it was at, and saying why a mapping in particular is dangerous rather than merely wrong. Nine lines and a test.

payload["features"] being absent is already refused, from #84. This is the other half of the same question: a key that is present and is not a page.

How it was verified

make verify green in full, determinism gate included. The measurement above is the evidence; a deliberate sabotage here would only be a restatement of it.

Part of the upstream contribution loop wildfire-service-territory-overlap's roadmap item 4.4 describes. That project pins a commit, so this reaches it only when the pin moves, deliberately.

Prepared with AI assistance; reviewed before submission.

yield from over a mapping yields its keys and len() of it counts them, so a
page whose features is an object made the walk emit the field names as though
they were features and step its offset by the number of them. Nothing raised.

Measured on the unmodified tree before the fix: a page carrying
{"features": {"OBJECTID": 1, "YEAR_": 2020}} makes iter_features yield
['OBJECTID', 'YEAR_'] and stop, after one request.

Found by the consuming project's own copy of this walk, which checks the type
and would have had to keep the check to move onto this one. A compensation
cannot be retired until the thing it compensates for is gone, so the check
belongs here rather than downstream.
@ChelseaKR
ChelseaKR merged commit b35a8d6 into main Sep 8, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant