---
title: "Error handling — Kubernetes API for Laravel | Roundly"
description: "Typed exceptions with body-free messages and Status accessors — apiserver errors, cluster config, namespace scope, rate limits, kubeconfig and exec."
url: https://roundly-consulting.com/open-source/docs/kubernetes-api-for-laravel/errors
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Kubernetes API for Laravel](https://roundly-consulting.com/open-source/docs/kubernetes-api-for-laravel.md)

# Error handling

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:

```php
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 body | Message | With redaction |
| --- | --- | --- |
| An apiserver Status with a message | `secrets "db" already exists` | `HTTP 409 AlreadyExists` |
| Anything else — an HTML page, an empty body… | `Kubernetes API request failed: HTTP 502` | `HTTP 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:

```php
$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

| Exception | Thrown when | Carries |
| --- | --- | --- |
| `KubernetesException` | The 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. |
| `RateLimitExceededException` | A 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(). |
| `KubeConfigException` | fromKubeConfig() 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. |
| `WebSocketException` | exec() 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. |
| `ClusterNotFoundException` | cluster($name) gets a name that is neither configured nor registered. | The unknown cluster name. |
| `ClusterConfigurationException` | A 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. |
| `NamespaceScopeException` | A namespace-scoped client or resource is asked to leave its namespace. | \->scope (the pinned namespace). |
| `InvalidResourceException` | registerResource() 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. |
| `InvalidConfigurationException` | A 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](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/kubernetes-api-for-laravel.md)
