Skip to content

[P2][bug] diesel.get_stations and the /test endpoints are POST-shaped reads that lost all retries (#115) #118

Description

@karlwaldman

Merged in #115 (fcc4edf). Verified by execution (the POST retry rule) plus reading (the call sites) against main on 2026-09-13 — confirmed live.

What is wrong

#115 correctly keys replay safety on the HTTP method: POST and PATCH are sent exactly once. But the SDK has POST-shaped reads — queries with a body — and no audit of its own call sites was done, so those lost all retry resilience.

Confirmed POST-shaped reads that now get exactly one attempt:

call site endpoint shape
oilpriceapi/resources/diesel.py:219 POST /v1/diesel-prices/stations query by {lat, lng, radius} — pure read
oilpriceapi/async_resources.py:65 same, async pure read
oilpriceapi/resources/webhooks.py:217 POST /v1/webhooks/{id}/test diagnostic
oilpriceapi/resources/alerts.py:538 POST /v1/alerts/{id}/test diagnostic

data_sources.rotate_credentials() and the various create() calls are genuine writes and correctly should not retry — no change wanted there.

Repro

import httpx, time
from oilpriceapi import OilPriceAPI

time.sleep = lambda s: None
FIXTURE_KEY = "fixture-not-a-real-credential"

calls = []
def handler(request):
    calls.append(str(request.url))
    return httpx.Response(503, json={"status": "error"})

c = OilPriceAPI(api_key=FIXTURE_KEY)
c._client = httpx.Client(base_url=c.base_url, headers=c.headers,
                         transport=httpx.MockTransport(handler))
try:
    c.diesel.get_stations(lat=37.7749, lng=-122.4194)
except Exception as e:
    print(type(e).__name__, "attempts:", len(calls))

Measured against the same shape via client.request("POST", ...): attempts = 1. A GET to the same transport gives attempts = 3.

Before #115 a transient 503 or connection reset on a station lookup was retried twice and usually succeeded. Now it fails on the first blip.

Why it matters

A station lookup is idempotent by construction — the same lat/lng/radius returns the same answer and creates nothing. Removing its retries is a pure availability regression with no safety benefit. Customer-visible as intermittent failures on a call that used to ride out a single bad gateway response.

Suggested fix

The mechanism #115 built is right; the call sites just need to use it. Pass idempotent=True at each read-shaped POST:

response = self.client.request(
    method="POST",
    path="/v1/diesel-prices/stations",
    json_data={"lat": lat, "lng": lng, "radius": radius},
    idempotent=True,
)

Worth adding a short note in CONTRIBUTING/the resource docstrings: any new POST that is a query must pass idempotent=True, and a test that walks the resource modules asserting every method="POST" call site either passes idempotent= explicitly or is on an allow-list of genuine writes, so the next POST-shaped read does not quietly lose its retries.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpriority: mediumShould be fixed eventually

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions