NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

Failed requests throw RoundlyConsulting\KubernetesApi\Exceptions\KubernetesException. It extends Laravel’s RequestException — catch (RequestException), $e->response and $e->getCode() (the HTTP status) keep working — and adds accessors that read the apiserver’s Status from the body on demand:

use RoundlyConsulting\KubernetesApi\Exceptions\KubernetesException;

try {
    $cluster->secrets()->setNamespace('shop')->setName('db')->create();
} catch (KubernetesException $e) {
    $e->status();       // 409 (also $e->getCode())
    $e->reason();       // 'AlreadyExists' — only a well-formed reason token, else null
    $e->apiMessage();   // 'secrets "db" already exists', or null
    $e->details();      // the Status details array (name, kind, causes…), or []
    $e->getMessage();   // 'secrets "db" already exists' — never the raw body
    $e->response;       // the full Laravel Response, body included
}

Messages never carry the body

Exception messages end up in logs and failed_jobs, so they never include the raw response body — an edge HTML page, or a proxy that echoes the Authorization header, would otherwise put the bearer token there. Laravel’s own body summary, which its handler writes into the message on report(), is never used:

Response bodyMessageWith redaction
An apiserver Status with a messagesecrets "db" already existsHTTP 409 AlreadyExists
Anything else — an HTML page, an empty body…Kubernetes API request failed: HTTP 502HTTP 502
  • The generic message adds the Status reason when the body has a well-formed one.
  • A redirect that wasn’t followed reads Kubernetes API request was redirected (HTTP 302 to https://login.example:8443) and the redirect was not followed — the target’s origin only, never its path or query.
  • A success answer the call can’t use throws KubernetesException::malformedResponse(): Malformed TokenRequest response from the Kubernetes API (HTTP 201).
  • new KubernetesException($response, ?string $message = null, bool $redacted = false) uses a message of your own as given.

Redacted errors

withRedactedErrors() — or redact_errors on a cluster’s config entry — cuts messages to status and reason, leaving out even the Status message. The accessors and $e->response still read the body, because asking for it is a choice, and applyConfig() keeps the setting:

$cluster = Kubernetes::cluster('production')->withRedactedErrors();
// or 'redact_errors' => true on the cluster's config entry (KUBERNETES_REDACT_ERRORS)

try {
    $cluster->secrets()->setNamespace('shop')->setName('db')->create();
} catch (KubernetesException $e) {
    $e->getMessage();   // 'HTTP 409 AlreadyExists'
    $e->apiMessage();   // still 'secrets "db" already exists' — reading it is a choice
}

Service-account token requests are always redacted, whatever the cluster says — it is a credential endpoint. A refused exec upgrade throws WebSocketException with the status line plus the apiserver’s Status message (the status line only when the cluster redacts), such as WebSocket upgrade failed: HTTP/1.1 403 Forbidden: pods "api" is forbidden; response headers and body are never included.

Every exception

ExceptionThrown whenCarries
KubernetesExceptionThe apiserver answers with a 4xx or 5xx, with a redirect that was not followed (redirectNotFollowed()), or with a success body the call can’t use (malformedResponse()).->response (the Laravel Response), status(), reason(), apiMessage() and details(); the message is the apiserver’s Status message — never the raw body.
RateLimitExceededExceptionA request would wait longer than rate_limits.max_wait.->cluster (the registered cluster name, or the apiserver host for an ad-hoc client) and retryAfterSeconds().
KubeConfigExceptionfromKubeConfig() or inCluster() cannot load credentials — including invalid YAML, an unreadable tokenFile, a user that signs in with an exec plugin, an auth provider or a username and password, a missing in-cluster CA certificate or an insecure-skip-tls-verify that isn’t a boolean.A message naming what is missing or invalid — for invalid YAML, the file and line only.
WebSocketExceptionexec() cannot connect or the WebSocket upgrade fails.The connection error, or the upgrade’s status line plus the apiserver’s Status message — never the raw headers or body.
ClusterNotFoundExceptioncluster($name) gets a name that is neither configured nor registered.The unknown cluster name.
ClusterConfigurationExceptionA cluster has no URL, a definition closure returns no Cluster, a config source is unknown, a cluster or client setting is malformed or out of range, or a resource is used without a cluster.Which cluster, config key or resource is misconfigured.
NamespaceScopeExceptionA namespace-scoped client or resource is asked to leave its namespace.->scope (the pinned namespace).
InvalidResourceExceptionregisterResource() or the fake’s seed() gets a class that is not a resource or an unusable name, the resources config isn’t an array or names a non-resource class, or a request’s name, namespace, plural or apiVersion can’t be a single URL path segment (nothing is sent). Typed builders throw it too: a label key or value outside the label grammar, a Traefik host or path rule input, a ClusterRoleBinding ServiceAccount subject without a namespace, a name-dependent helper (Volume::fromSecret(), Job::podsSelectors(), Service::getClusterDns()) on an unnamed resource, and requestToken() input the apiserver would refuse.The offending class, name or value.
InvalidConfigurationExceptionA rate_limits key or traefik.group is invalid — an unrecognised switch, a non-integer or out-of-range number, an unknown timespan or a non-string value. It comes from package-toolkit-for-laravel.A message naming the key and the value given.

A 404 from find() is the usual signal to create — existsOnCluster() and updateOrCreate() already handle it for you. A 409 from update() or updateOrCreate() means the object changed since you read it: re-read and retry.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.