Repository navigation
fix: keep the HTTP status on API errors without a JSON Status body - #32
Merged
Merged
Conversation
KubernetesAPIException took its code only from the `code` field of the decoded response body. When the API server answers a 4xx with something other than a JSON Status, the code became 0 and the HTTP status was lost. That is what it does for an API group or resource it does not serve: a GET under snapshot.storage.k8s.io on a cluster without the VolumeSnapshot CRDs answers a plain-text "404 page not found". Every `getCode() === 404` check, K8sResource::exists() included, then missed the 404. appwrite-labs/edge hit this on kind clusters without the snapshot CRDs. Branch cleanup read the missing VolumeSnapshot as a hard failure rather than as absent, left orphan branch namespaces behind, and failed cloud's CI. The Status code still wins when the body carries one (a Status code of 0 means unset). Otherwise the exception takes the HTTP status. JSON Status payloads pass through unchanged. The Guzzle exception is now chained as the previous exception, so the raw response stays reachable, and the cluster version check builds its exception the same way. PhpK8sException gains an optional trailing `$previous` argument, which keeps existing callers compatible, and it stops passing a null message to Exception, which PHP 8.1+ deprecates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
… cluster The JSON Status cases sent the same value as the HTTP status and the body `code`, so they passed whichever source the exception read its code from. They could not catch a regression to "always use the HTTP status" or to "always use the body". Each case now sends distinct values: a positive body code beats the HTTP status, a body code of 0 (unset in a Kubernetes Status) falls back to it, and a plain-text body takes it. Swapping from() to always read the HTTP status, or dropping the zero check, now turns a test red. The live check for an unserved API group raised a connection error instead of skipping when no API server is reachable. It now sits in its own integration test class with the same setUp guard as the other live tests, so CI always runs it and local runs without a proxy skip it. The probe catches Exception, as ExecCredentialIntegrationTest does. The variants that catch only KubernetesAPIException never see the ConnectException raised by a refused connection, so they error instead of skipping. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
MakesHttpCalls::call()builtKubernetesAPIExceptionwith$errorPayload['code'] ?? 0. A 4xx whose body is not a JSONStatustherefore came back with code0: the HTTP status was lost and the Guzzle exception was not chained. The API server answers that way for an API group or resource it does not serve. For example,GET /apis/snapshot.storage.k8s.io/v1/…/volumesnapshots/…on a cluster without the VolumeSnapshot CRDs answers a plain-text404 page not found, and everygetCode() === 404check misses it,K8sResource::exists()included.Edge impact: on kind clusters without the snapshot CRDs, edge's branch cleanup read that 404 as a hard failure (code 0) instead of "absent". It left orphan branch namespaces behind and failed cloud's CI.
Changes
KubernetesAPIException::from(BadResponseException)builds the exception in one place:codewhen it's a positive int, so JSON Status bodies behave as before. Otherwise the HTTP status. A Statuscodeof 0 means unset.null.MakesHttpCalls::call()andChecksClusterVersion::loadClusterVersion()now use it. They were the only two places that wrapped a GuzzleClientExceptionthis way.PhpK8sException::__construct()takes an optional trailing?Throwable $previous, so existing callers are unaffected. It also passes''instead ofnulltoException, because PHP 8.1+ deprecatesnullthere.Behaviour notes
K8sResource::exists()now returnsfalsefor a resource under an API group the cluster doesn't serve. Before, it threw a code-0KubernetesAPIException.appwrite-labs/php-k8s(it pinsdev-main):Pooler::transient()deliberately retries code-0 errors, and its docblock describes the old behaviour. A non-Status 4xx will now carry its real status and be treated as definitive.'404 Not Found'message fallback inNamespaceIdentity::absent()becomes redundant.Tests
tests/MakesHttpCallsTest.php(unit): a GuzzleMockHandlerdrivescall()through agetClient()override,TestCase::clusterRespondingWith(). Each case uses a different HTTP status and bodycode, so the tests show which one wins:code: 409gives code 409, because the body wins. The payload comes through unchanged.code: 0gives code 404, because a zero body code falls back to the HTTP status. The payload comes through unchanged.text/plain404 page not foundgives code 404, anullpayload and a chainedClientException.tests/ChecksClusterVersionTest.php(unit): atext/plain403 from/versiongives code 403 and a chainedClientException.tests/MakesHttpCallsIntegrationTest.php(integration, runs on minikube in CI): a GET under an unserved API group raises aKubernetesAPIExceptionwith code 404. It uses the samesetUp()guard as the other live tests, so a local run without a cluster skips it.Red/green:
src/change, the plain-text cases fail withFailed asserting that 0 is identical to 404.and… 0 is identical to 403.. After it, they pass.from()always return the HTTP status fails the body-wins case withFailed asserting that 400 is identical to 409.Failed asserting that 0 is identical to 404.Local checks (PHP 8.5, no cluster, no ext-yaml):
pint --testpasses.psalmreports the same 571 issues before and after this change. They're mostly unused-code findings, and CI doesn't run Psalm.phpunit:main: 95 tests need a cluster and 61 need ext-yaml.🤖 Generated with Claude Code