Throw ValidationFailedException with per-field codes for 422 responses

This commit is contained in:
envoyr committed 2026-11-07 00:33:38 +00:00
1 parent a117a6a3c9
commit d458d5d7fc
4 files changed
+119 -2

No files matched your search

+5
View File
@@ -35,6 +35,11 @@ class ApiException extends ResourceException
return PreconditionFailedException::fromResult($result);
}
// refused input, with per-field codes on some endpoints
if ($status === 422 && static::class === self::class) {
return ValidationFailedException::fromResult($result);
}
return new static(
rtrim((string) ($body->message ?? $result->error()), '.'),
$status,
@@ -0,0 +1,46 @@
<?php
namespace Anikeen\Id\Exceptions;
/**
* Anikeen ID refused the input (422). `error` is the first code; endpoints with per-field codes
* (the profile API) send them as `codes`: `{field: code}`, e.g. `["username" => "username_taken"]`.
* Branch on the codes, the messages are for people and may change.
*/
class ValidationFailedException extends ApiException
{
/**
* @return array<string, string> field => code; empty when the endpoint sends none
*/
public function errorCodes(): array
{
$codes = $this->body()->codes ?? null;
return is_object($codes) || is_array($codes) ? array_map('strval', (array) $codes) : [];
}
public function errorCodeFor(string $field): ?string
{
return $this->errorCodes()[$field] ?? null;
}
/**
* Whether any field failed with $code, or the response's `error` is $code.
*/
public function hasErrorCode(string $code): bool
{
return $this->error === $code || in_array($code, $this->errorCodes(), true);
}
/**
* The messages per field (`errors`), for showing them to people.
*
* @return array<string, list<string>>
*/
public function errors(): array
{
$errors = $this->body()->errors ?? null;
return is_object($errors) || is_array($errors) ? array_map(fn ($messages) => array_values((array) $messages), (array) $errors) : [];
}
}
+7 -2
View File
@@ -6,6 +6,7 @@ use Anikeen\Id\AnikeenId;
use Anikeen\Id\Contracts\AppTokenRepository;
use Anikeen\Id\Exceptions\ApiException;
use Anikeen\Id\Exceptions\PreconditionFailedException;
use Anikeen\Id\Exceptions\ValidationFailedException;
use Anikeen\Id\Result;
use GuzzleHttp\Exception\GuzzleException;
use Throwable;
@@ -13,7 +14,7 @@ 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 (a 412 in a
* PreconditionFailedException).
* PreconditionFailedException, a 422 in a ValidationFailedException).
*/
final class AppRequest
{
@@ -36,7 +37,11 @@ final class AppRequest
if (!$result->success()) {
$body = is_object($result->data) ? $result->data : null;
$exception = $result->status === 412 ? PreconditionFailedException::class : ApiException::class;
$exception = match ($result->status) {
412 => PreconditionFailedException::class,
422 => ValidationFailedException::class,
default => ApiException::class,
};
throw new $exception(
sprintf('Anikeen ID request %s %s failed: %s', $method, $path, $body->message ?? $result->error()),