Send extra request headers and throw PreconditionFailedException on 412

This commit is contained in:
envoyr committed 2026-11-06 19:25:51 +00:00
1 parent 9c505842cb
commit d76095cc94
5 files changed
+164 -18

No files matched your search

+12 -10
View File
@@ -369,10 +369,12 @@ class AnikeenId
/**
* Build query & execute.
*
* @param array<string, string> $headers Extra request headers, e.g. `If-Match` for conditional
* writes; they are sent in addition to (and may not replace) Client-ID, Accept and Authorization.
* @throws GuzzleException
* @throws RequestRequiresClientIdException
*/
public function request(string $method, string $path, null|array $payload = null, array $parameters = [], ?Paginator $paginator = null, bool $useClientSecret = false): Result
public function request(string $method, string $path, null|array $payload = null, array $parameters = [], ?Paginator $paginator = null, bool $useClientSecret = false, array $headers = []): Result
{
if ($paginator !== null) {
$parameters[$paginator->action] = $paginator->cursor();
@@ -380,7 +382,7 @@ class AnikeenId
try {
$response = $this->client->request($method, $path, [
'headers' => $this->buildHeaders((bool)$payload, $useClientSecret),
'headers' => $this->buildHeaders((bool)$payload, $useClientSecret) + $headers,
'query' => Query::build($parameters),
'json' => $payload ?: null,
]);
@@ -419,35 +421,35 @@ class AnikeenId
* @throws GuzzleException
* @throws RequestRequiresClientIdException
*/
public function get(string $path, array $parameters = [], ?Paginator $paginator = null): Result
public function get(string $path, array $parameters = [], ?Paginator $paginator = null, array $headers = []): Result
{
return $this->request('GET', $path, null, $parameters, $paginator);
return $this->request('GET', $path, null, $parameters, $paginator, false, $headers);
}
/**
* @throws GuzzleException
* @throws RequestRequiresClientIdException
*/
public function post(string $path, array $payload = [], array $parameters = [], ?Paginator $paginator = null): Result
public function post(string $path, array $payload = [], array $parameters = [], ?Paginator $paginator = null, array $headers = []): Result
{
return $this->request('POST', $path, $payload, $parameters, $paginator);
return $this->request('POST', $path, $payload, $parameters, $paginator, false, $headers);
}
/**
* @throws GuzzleException
* @throws RequestRequiresClientIdException
*/
public function put(string $path, array $payload = [], array $parameters = [], ?Paginator $paginator = null): Result
public function put(string $path, array $payload = [], array $parameters = [], ?Paginator $paginator = null, array $headers = []): Result
{
return $this->request('PUT', $path, $payload, $parameters, $paginator);
return $this->request('PUT', $path, $payload, $parameters, $paginator, false, $headers);
}
/**
* @throws GuzzleException
* @throws RequestRequiresClientIdException
*/
public function delete(string $path, array $payload = [], array $parameters = [], ?Paginator $paginator = null): Result
public function delete(string $path, array $payload = [], array $parameters = [], ?Paginator $paginator = null, array $headers = []): Result
{
return $this->request('DELETE', $path, $payload, $parameters, $paginator);
return $this->request('DELETE', $path, $payload, $parameters, $paginator, false, $headers);
}
}
+7 -1
View File
@@ -28,10 +28,16 @@ class ApiException extends ResourceException
public static function fromResult(Result $result): static
{
$body = is_object($result->data) ? $result->data : null;
$status = $result->response()?->getStatusCode() ?? 0;
// a failed conditional write; subclasses (CollectionException) keep their own type
if ($status === 412 && static::class === self::class) {
return PreconditionFailedException::fromResult($result);
}
return new static(
rtrim((string) ($body->message ?? $result->error()), '.'),
$result->response()?->getStatusCode() ?? 0,
$status,
$body->error ?? null,
$result->exception,
);
@@ -0,0 +1,21 @@
<?php
namespace Anikeen\Id\Exceptions;
/**
* A conditional write failed (412): the stored version wasn't the one sent with `If-Match`
* ("preference_version_mismatch"), or the key was already set and the write was create-only
* ("preference_exists"). Reload, merge and retry with currentVersion().
*/
class PreconditionFailedException extends ApiException
{
/**
* The version stored now, null when the key isn't set (or Anikeen ID didn't send one).
*/
public function currentVersion(): ?int
{
$version = $this->body()->current_version ?? null;
return is_numeric($version) ? (int) $version : null;
}
}
+11 -7
View File
@@ -5,36 +5,40 @@ namespace Anikeen\Id\Support;
use Anikeen\Id\AnikeenId;
use Anikeen\Id\Contracts\AppTokenRepository;
use Anikeen\Id\Exceptions\ApiException;
use Anikeen\Id\Exceptions\PreconditionFailedException;
use Anikeen\Id\Result;
use GuzzleHttp\Exception\GuzzleException;
use Throwable;
/**
* Sends a request with the app's own token (client credentials). A rejected token is renewed
* once; every failure ends in an ApiException with status and error code.
* once; every failure ends in an ApiException with status and error code (a 412 in a
* PreconditionFailedException).
*/
final class AppRequest
{
/**
* @param array<string, string> $headers Extra request headers, e.g. `If-Match`.
* @throws ApiException
*/
public static function send(string $method, string $path, ?array $payload = null, array $query = []): Result
public static function send(string $method, string $path, ?array $payload = null, array $query = [], array $headers = []): Result
{
$result = self::attempt($method, $path, $payload, $query);
$result = self::attempt($method, $path, $payload, $query, $headers);
if ($result->status === 401) {
$tokens = app(AppTokenRepository::class);
if (method_exists($tokens, 'forgetAccessToken')) {
$tokens->forgetAccessToken();
$result = self::attempt($method, $path, $payload, $query);
$result = self::attempt($method, $path, $payload, $query, $headers);
}
}
if (!$result->success()) {
$body = is_object($result->data) ? $result->data : null;
$exception = $result->status === 412 ? PreconditionFailedException::class : ApiException::class;
throw new ApiException(
throw new $exception(
sprintf('Anikeen ID request %s %s failed: %s', $method, $path, $body->message ?? $result->error()),
$result->response()?->getStatusCode() ?? 0,
$body->error ?? null,
@@ -48,10 +52,10 @@ final class AppRequest
/**
* @throws ApiException
*/
private static function attempt(string $method, string $path, ?array $payload, array $query): Result
private static function attempt(string $method, string $path, ?array $payload, array $query, array $headers = []): Result
{
try {
return app(AnikeenId::class)->asApp()->request($method, $path, $payload, $query);
return app(AnikeenId::class)->asApp()->request($method, $path, $payload, $query, null, false, $headers);
} catch (ApiException $e) {
throw $e;
} catch (GuzzleException|Throwable $e) {