From 589286fbd1e465f7d38399796de05a5b00c1d6d2 Mon Sep 17 00:00:00 2001 From: Sahil Khatri <88265012+sahilkhatri09@users.noreply.github.com> Date: Thu, 29 Oct 2026 12:19:02 +0530 Subject: [PATCH] Keep received exec exit status when the connection fails on close The API server can close the exec WebSocket right after sending the exit status. ExecProcess then closes its side, writing the close frame fails (e.g. "Connection or outbound has been closed"), and failure() overwrote the already received exit status with -1975219, so a command that finished was reported as a failure. Only set -1975219 when no exit status was received. --- .../main/java/io/kubernetes/client/Exec.java | 13 +++++++---- .../java/io/kubernetes/client/ExecTest.java | 23 +++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/util/src/main/java/io/kubernetes/client/Exec.java b/util/src/main/java/io/kubernetes/client/Exec.java index 31201ff22f..2eadc70667 100644 --- a/util/src/main/java/io/kubernetes/client/Exec.java +++ b/util/src/main/java/io/kubernetes/client/Exec.java @@ -556,10 +556,15 @@ public void failure(Throwable ex) { ExecProcess.this.onUnhandledError.accept(ex); synchronized (ExecProcess.this) { - // Try for a pretty unique error code, so if someone searches - // they'll find this - // code. - statusCode = -1975219; + // The server may close the connection right after sending the exit status, so + // closing our side can fail after the command finished. Keep a received exit + // status in that case. + if (statusCode < 0) { + // Try for a pretty unique error code, so if someone searches + // they'll find this + // code. + statusCode = -1975219; + } isAlive = false; ExecProcess.this.latch.countDown(); } diff --git a/util/src/test/java/io/kubernetes/client/ExecTest.java b/util/src/test/java/io/kubernetes/client/ExecTest.java index 5d3b5cf8ef..8d3b380563 100644 --- a/util/src/test/java/io/kubernetes/client/ExecTest.java +++ b/util/src/test/java/io/kubernetes/client/ExecTest.java @@ -204,6 +204,29 @@ void customUnhandledError() throws IOException, InterruptedException { assertThat(process.exitValue()).isEqualTo(EXPECTED_ERROR_EXIT_CODE); } + @ParameterizedTest + @MethodSource("exitStatusBeforeFailureData") + void unhandledErrorAfterExitStatusKeepsExitCode(String status, int expectedExitCode) + throws IOException, InterruptedException { + final Consumer consumer = mock(Consumer.class); + final Throwable throwable = mock(Throwable.class); + final ExecProcess process = new ExecProcess(client, consumer); + process.getHandler().open("wss", null); + + process.getHandler().bytesMessage(makeStream(3, status.getBytes(StandardCharsets.UTF_8))); + // e.g. sending the close frame fails because the server already closed the connection + process.getHandler().failure(throwable); + process.waitFor(); + + verify(consumer, times(1)).accept(throwable); + assertThat(process.isAlive()).isFalse(); + assertThat(process.exitValue()).isEqualTo(expectedExitCode); + } + + private static Stream exitStatusBeforeFailureData() { + return Stream.of(arguments(OUTPUT_EXIT0, 0), arguments(OUTPUT_EXIT126, 126)); + } + @Test void url() throws IOException, ApiException, InterruptedException { final Consumer consumer = mock(Consumer.class);