*/ private array $remote_data; /** * @param int $code HttpClient's recognition bucket. * @param int $http_status 0 when there was no response. * @param bool|null $retryable Overrides the default verdict. * @param array $remote_data The remote's structured detail. */ public function __construct( string $message, int $code = 0, int $http_status = 0, string $remote_code = '', ?bool $retryable = null, ?Throwable $previous = null, array $remote_data = [] ) { parent::__construct( $message, $code, $previous ); $this->http_status = $http_status; $this->remote_code = $remote_code; // Transport failures (DNS, refused, timeout, SSL) have no status and count as // transient; only an override or a known status narrows that. $this->retryable = $retryable ?? ( 0 === $http_status || self::status_is_retryable( $http_status ) ); $this->remote_data = $remote_data; } /** HTTP status, or 0 when the request never produced a response. */ public function http_status(): int { return $this->http_status; } /** The remote's machine-readable error code, or '' when it sent none. */ public function remote_code(): string { return $this->remote_code; } /** * The remote's structured error detail, never null. * * Untrusted: it is whatever the other site put in its error body, so it is * redacted before reaching a log or support export. * * @return array */ public function remote_data(): array { return $this->remote_data; } /** True when the same request could plausibly succeed on a second attempt. */ public function is_retryable(): bool { return $this->retryable; } /** * Whether an HTTP status is worth retrying: 408, 429 and 5xx. Any other 4xx is * the server refusing this exact request, so repeating it only delays the failure. */ public static function status_is_retryable( int $status ): bool { return 408 === $status || 429 === $status || $status >= 500; } }