Related to #111 (6e1a66c) and issue #112. Verified by execution against main on 2026-09-13 — confirmed live.
What is wrong
oilpriceapi/resources/_futures_slug.py:148 raises a builtin ValueError, which is not in the SDK's exception hierarchy:
raise ValueError(
f"Unknown futures contract/slug {contract!r}. "
f"Pass a slug ({valid}) or a contract code ({codes})."
)
Every FuturesResource method calls normalize_futures_slug before issuing a request (futures.py:58, 94, 129, 159, 212, 257), so this escapes straight out of the public API.
Repro
from oilpriceapi.resources.futures import FuturesResource
from oilpriceapi.exceptions import OilPriceAPIError
class FakeClient:
def request(self, **kw): return {}
for m in ["latest", "curve", "intraday"]:
try:
getattr(FuturesResource(FakeClient()), m)("WTI_MIDLAND_USD")
except OilPriceAPIError as e:
print(m, "caught:", type(e).__name__)
except Exception as e:
print(m, "ESCAPES OilPriceAPIError ->", type(e).__name__)
Measured:
latest: ESCAPES OilPriceAPIError -> ValueError
curve: ESCAPES OilPriceAPIError -> ValueError
intraday: ESCAPES OilPriceAPIError -> ValueError
Why it matters
The ValueError predates #111, but #111 changed 18 live catalog codes from "resolved to the wrong instrument" to "raises" (LNG_NW_EUROPE_EUR, WTI_MIDLAND_USD, TTF_NL_EUR, LNG_JKM_USD, NATURAL_GAS_USD, BRENT_CRUDE_USD, GASOIL_USD, … all verified raising). That is the right call — but it means user code shaped like:
try:
curve = client.futures.curve(code)
except OilPriceAPIError:
fall_back()
goes from "silently wrong answer" to "uncaught crash". #111's own comment argues "a refusal is recoverable, a wrong instrument's curve is not" — it is only recoverable if the refusal is catchable through the documented base class.
Suggested fix
Raise oilpriceapi.exceptions.ValidationError (already a subclass of OilPriceAPIError) instead, with field="contract" and value=contract. Keep the "unknown slug" text as the message — but note #117: ValidationError.__str__ currently discards a supplied message whenever field is set, so that needs fixing first or the helpful list of valid slugs will not be printed.
ValueError should be retained as a base if backward compatibility matters — class FuturesContractError(ValidationError, ValueError) catches both old and new call sites.
Residual, same file (low)
_futures_slug.py:140 still strips trailing digits unconditionally, after the new month-suffix guard has already decided not to split:
WTI2026 -> wti BRENT1 -> brent LNG1 -> lng-jkm CL0 -> wti
Same "guess rather than refuse" shape #111 removed from the separator split. Low impact — no live catalog code matches — but worth closing while the file is open.
Related to #111 (
6e1a66c) and issue #112. Verified by execution againstmainon 2026-09-13 — confirmed live.What is wrong
oilpriceapi/resources/_futures_slug.py:148raises a builtinValueError, which is not in the SDK's exception hierarchy:Every
FuturesResourcemethod callsnormalize_futures_slugbefore issuing a request (futures.py:58, 94, 129, 159, 212, 257), so this escapes straight out of the public API.Repro
Measured:
Why it matters
The
ValueErrorpredates #111, but #111 changed 18 live catalog codes from "resolved to the wrong instrument" to "raises" (LNG_NW_EUROPE_EUR,WTI_MIDLAND_USD,TTF_NL_EUR,LNG_JKM_USD,NATURAL_GAS_USD,BRENT_CRUDE_USD,GASOIL_USD, … all verified raising). That is the right call — but it means user code shaped like:goes from "silently wrong answer" to "uncaught crash". #111's own comment argues "a refusal is recoverable, a wrong instrument's curve is not" — it is only recoverable if the refusal is catchable through the documented base class.
Suggested fix
Raise
oilpriceapi.exceptions.ValidationError(already a subclass ofOilPriceAPIError) instead, withfield="contract"andvalue=contract. Keep the "unknown slug" text as the message — but note #117:ValidationError.__str__currently discards a suppliedmessagewheneverfieldis set, so that needs fixing first or the helpful list of valid slugs will not be printed.ValueErrorshould be retained as a base if backward compatibility matters —class FuturesContractError(ValidationError, ValueError)catches both old and new call sites.Residual, same file (low)
_futures_slug.py:140still strips trailing digits unconditionally, after the new month-suffix guard has already decided not to split:Same "guess rather than refuse" shape #111 removed from the separator split. Low impact — no live catalog code matches — but worth closing while the file is open.