fix(subscriptions): refuse malformed list/events 200s and replaying cursors (#142) - #147
Merged
Merged
Conversation
…ursors (#142) subscriptions.list() and events(), sync and async, unwrapped with data.get("subscriptions", []) / data.get("events", []) / data.get("cursor"), so a 200 without those keys read as "no subscriptions" / "no new events" with cursor=None. Fed back as events(since=page.cursor), None dropped since; the API reads params[:since].to_i, so the poller replayed every event. - unwrap_subscription_list / unwrap_events_page in _subscriptions_common, shared by both clients, raise OilPriceAPIError(MALFORMED_RESPONSE) with the raw body for a missing or mistyped collection, an invalid record, a non-integer or negative cursor, a non-boolean has_more, or a cursor behind since or behind an event in the page. They reuse _malformed from _fuel_surcharge_common. An empty list or page the API actually sent is still an empty success. - validate_since refuses since values the API reads as 0 or truncates ("abc", "", "41", -1, 1.5, True) with ValidationError(field="since", status_code=None) before any request. Closes #142 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ao5paex73xXvuM424Libo
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
karlwaldman
added a commit
that referenced
this pull request
Sep 13, 2026
Resolve the CHANGELOG ### Fixed conflict with #147 by keeping both entries. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ao5paex73xXvuM424Libo
This was referenced Sep 13, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
subscriptions.list()andsubscriptions.events()(sync and async) unwrapped withdata.get("subscriptions", []),data.get("events", [])anddata.get("cursor"). A 200 without those keys read as "no subscriptions" / "no new events" withcursor=None. Fed back through the documented loopevents(since=page.cursor), thatNonedroppedsince, and the poller replayed the account's whole event history.unwrap_subscription_listandunwrap_events_pageinoilpriceapi/_subscriptions_common.pyare used by both clients. They follow theunwrap_subscriptionstyle and reuse the existing_malformedfrom_fuel_surcharge_common; no new error helper. Each raisesOilPriceAPIError(code="MALFORMED_RESPONSE", raw_body=...)when:data.subscriptionsordata.eventsis missing or not a list;data.cursoris not a non-negativeint(bool excluded);data.has_moreis not abool;since, or behind an event'sseqin the same page, since following it would replay events.validate_sincerefuses asincethe API would read as0or truncate, before any request is sent. Refused values:"abc","","41",-1,1.5,True,[41]. It raisesValidationError(field="since", status_code=None).None(first poll) and0stay allowed.{"subscriptions": []}or{"events": [], "cursor": 41, "has_more": false}is still an empty success, andpage.cursoris now always anint.No pydantic validator was added and no raw
ValueErroris raised.What the API actually does with the cursor
V1::SubscriptionsControlleron oilpriceapi-apiorigin/main:#eventssetssince = params[:since].to_i, thencursor = events.last&.seq || since, and always renders{cursor, has_more, events}.#indexalways renders{subscriptions: [...]}.Live probe against
api.oilpriceapi.comon 2026-09-13, using the test account, which has 101 events:sincesentseqcursorhas_more0abc-11.5999999999A missing or invalid cursor is accepted with a 200 and silently restarts from event 1. The server never returns an error for it, so the SDK has to refuse it. The
999999999row is a separate API-side hazard, filed as OilpriceAPI/oilpriceapi-api#8491 together with the silent replay: a cursor past the latest event is echoed back and the poller never sees new events.Nothing was created. The probe only issued GETs against the events route.
Red (tests written first, run on unchanged
origin/maina5304b3)Failure reasons, counted:
The 10 that passed on main are the success paths (live list body, live events page, empty page, first poll without
since), kept as regression guards.Green
Full suite. Baseline re-measured on
origin/maina5304b3 with all extras installed:origin/mainThe 3 failures are the live
tests/integration/test_demo_contract.py429s on both runs.ruff check oilpriceapi/: All checks passed!mypy oilpriceapi/ --ignore-missing-imports, in a CI-equivalent.[dev]environment: Success, no issues found in 55 source files.python scripts/validate_storefront_claims.py: validated 76 public surfaces.Merge order
Both this PR and the #145 PR add entries under
## [Unreleased]/### FixedinCHANGELOG.md. Merge this one first. The #145 branch then takes a merge oforigin/main(no rebase) to resolve the CHANGELOG hunk.Closes #142
🤖 Generated with Claude Code
https://claude.ai/code/session_015ao5paex73xXvuM424Libo