Confirmed by execution on main (3feb505), PHP 8.5.8, 2026-09-13.
Client::request() decides whether to attach the API key with str_starts_with($path, '/v1/demo') (src/Client.php:241). Any path that merely begins with that string is treated as keyless demo traffic, so the Authorization header is dropped and the server answers 401 — and the SDK then tells the customer their key is invalid.
Repro
<?php
require 'vendor/autoload.php';
require 'tests/MockTransport.php';
use OilPriceAPI\Client;
use OilPriceAPI\Tests\MockTransport;
foreach (['/v1/demo/prices', '/v1/demographics', '/v1/demo/../prices/latest', '/v1/prices/latest'] as $p) {
$t = new MockTransport();
$t->queue(200, ['status' => 'success', 'data' => ['code' => 'X', 'price' => 1.0]]);
(new Client('SECRET_KEY', 'https://api.oilpriceapi.com', 10.0, 0, $t))->raw()->get($p);
printf("%-28s auth-header=%s\n", $p, var_export($t->requests[0]['headers']['Authorization'] ?? null, true));
}
/v1/demo/prices auth-header=NULL
/v1/demographics auth-header=NULL <-- key silently withheld
/v1/demo/../prices/latest auth-header=NULL <-- key silently withheld
/v1/prices/latest auth-header='Token SECRET_KEY'
Impact
A customer with a perfectly good key calls raw()->get() on a path that happens to start with /v1/demo, gets a 401, and the SDK raises AuthenticationException reading "Invalid API key. Create or manage an API key at https://www.oilpriceapi.com/auth/signup...". That is a support ticket whose premise is wrong. No data harm and no credential harm — the failure is in the safe direction — but the diagnosis handed to the user is false.
Suggested fix
Match the demo endpoint exactly rather than by prefix — $path === '/v1/demo/prices', or a segment-boundary check ($path === '/v1/demo' || str_starts_with($path, '/v1/demo/')) plus a rejection of .. segments in normalizeApiPath(), which would also make /v1/demo/../prices/latest an error instead of a confusing 401.
Found during the post-merge PHP expert review of #17/#18/#19.
Confirmed by execution on
main(3feb505), PHP 8.5.8, 2026-09-13.Client::request()decides whether to attach the API key withstr_starts_with($path, '/v1/demo')(src/Client.php:241). Any path that merely begins with that string is treated as keyless demo traffic, so theAuthorizationheader is dropped and the server answers 401 — and the SDK then tells the customer their key is invalid.Repro
Impact
A customer with a perfectly good key calls
raw()->get()on a path that happens to start with/v1/demo, gets a 401, and the SDK raisesAuthenticationExceptionreading "Invalid API key. Create or manage an API key at https://www.oilpriceapi.com/auth/signup...". That is a support ticket whose premise is wrong. No data harm and no credential harm — the failure is in the safe direction — but the diagnosis handed to the user is false.Suggested fix
Match the demo endpoint exactly rather than by prefix —
$path === '/v1/demo/prices', or a segment-boundary check ($path === '/v1/demo' || str_starts_with($path, '/v1/demo/')) plus a rejection of..segments innormalizeApiPath(), which would also make/v1/demo/../prices/latestan error instead of a confusing 401.Found during the post-merge PHP expert review of #17/#18/#19.