From 06b3ce1282c5790e7750d879cec16532911d49d5 Mon Sep 17 00:00:00 2001 From: Andrei Solntsev Date: Fri, 13 Mar 2026 11:17:34 +0200 Subject: [PATCH 1/5] #4096 don't add http header "Content-Type" to GET request it's nonsense because GET requests don't have BODY (while "Content-Type" describes the request body). Apparently, the intention was to specify expected response type. Then http header "Accept" should be used instead. But anyway, docker ignores it, and always responds in "application/vnd.docker.multiplexed-stream" format (or "application/octet-stream" for older docker versions). --- .../org/openqa/selenium/docker/client/GetContainerLogs.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/java/src/org/openqa/selenium/docker/client/GetContainerLogs.java b/java/src/org/openqa/selenium/docker/client/GetContainerLogs.java index 81fb12b4889ef..ecb59595b6eae 100644 --- a/java/src/org/openqa/selenium/docker/client/GetContainerLogs.java +++ b/java/src/org/openqa/selenium/docker/client/GetContainerLogs.java @@ -47,10 +47,9 @@ public ContainerLogs apply(ContainerId id) { String requestUrl = String.format("/v%s/containers/%s/logs?stdout=true&stderr=true", apiVersion, id); - HttpResponse res = - client.execute(new HttpRequest(GET, requestUrl).addHeader("Content-Type", "text/plain")); + HttpResponse res = client.execute(new HttpRequest(GET, requestUrl)); if (res.getStatus() != HTTP_OK) { - LOG.warning("Unable to inspect container " + id); + LOG.warning(() -> "Unable to inspect container " + id); } List logLines = List.of(Contents.string(res).split("\n")); return new ContainerLogs(id, logLines); From 5d1fcf04eacda64aefd3356a95df01d910aa05e3 Mon Sep 17 00:00:00 2001 From: Andrei Solntsev Date: Fri, 13 Mar 2026 11:24:45 +0200 Subject: [PATCH 2/5] #4096 don't store logs of already stopped container There was a bug that method "stop" was called twice (first time regular, and the second time - from some "cache expiration listener" in Grid). The second call could not get container logs anymore, and overrode the logs file with empty content. --- java/src/org/openqa/selenium/docker/Container.java | 4 ++++ .../openqa/selenium/grid/node/docker/DockerSession.java | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/java/src/org/openqa/selenium/docker/Container.java b/java/src/org/openqa/selenium/docker/Container.java index 6d03663e45206..cbe16a7cba964 100644 --- a/java/src/org/openqa/selenium/docker/Container.java +++ b/java/src/org/openqa/selenium/docker/Container.java @@ -74,4 +74,8 @@ public ContainerLogs getLogs() { } return new ContainerLogs(getId(), new ArrayList<>()); } + + public boolean isRunning() { + return running; + } } diff --git a/java/src/org/openqa/selenium/grid/node/docker/DockerSession.java b/java/src/org/openqa/selenium/grid/node/docker/DockerSession.java index 464be03e2ab5d..ddc2cfe1ee8bd 100644 --- a/java/src/org/openqa/selenium/grid/node/docker/DockerSession.java +++ b/java/src/org/openqa/selenium/grid/node/docker/DockerSession.java @@ -17,6 +17,8 @@ package org.openqa.selenium.grid.node.docker; +import static java.util.logging.Level.FINE; + import java.net.URL; import java.nio.file.Files; import java.nio.file.Paths; @@ -72,6 +74,12 @@ public void stop() { } private void saveLogs() { + if (!container.isRunning()) { + LOG.log( + FINE, () -> "Skip saving logs because container is not running: " + container.getId()); + return; + } + String sessionAssetsPath = assetsPath.getContainerPath(getId()); String seleniumServerLog = String.format("%s/selenium-server.log", sessionAssetsPath); try { From f7343f796fee31fc34698bbf6d469d869c225927 Mon Sep 17 00:00:00 2001 From: Andrei Solntsev Date: Fri, 13 Mar 2026 11:35:06 +0200 Subject: [PATCH 3/5] #4096 store docker logs effectively instead of reading the whole response into memory, just copy the InputStream from "/logs" docker response to "selenium-server.log" file. NB! It's still incorrect solution because docker "/logs" response has a special format "application/vnd.docker.multiplexed-stream", and we need to parse it accordingly. Namely, it contains few "technical" bytes in the beginning of every log line. --- .../org/openqa/selenium/docker/Container.java | 4 +-- .../openqa/selenium/docker/ContainerLogs.java | 32 ++++++++++++++--- .../docker/client/GetContainerLogs.java | 6 ++-- .../grid/node/docker/DockerSession.java | 36 +++++++++++++++---- .../remote/http/jdk/JdkHttpMessages.java | 8 ++++- 5 files changed, 67 insertions(+), 19 deletions(-) diff --git a/java/src/org/openqa/selenium/docker/Container.java b/java/src/org/openqa/selenium/docker/Container.java index cbe16a7cba964..1407e0f90dbfe 100644 --- a/java/src/org/openqa/selenium/docker/Container.java +++ b/java/src/org/openqa/selenium/docker/Container.java @@ -18,10 +18,10 @@ package org.openqa.selenium.docker; import java.time.Duration; -import java.util.ArrayList; import java.util.logging.Level; import java.util.logging.Logger; import org.openqa.selenium.internal.Require; +import org.openqa.selenium.remote.http.Contents; public class Container { @@ -72,7 +72,7 @@ public ContainerLogs getLogs() { LOG.info("Getting logs " + getId()); return protocol.getContainerLogs(getId()); } - return new ContainerLogs(getId(), new ArrayList<>()); + return new ContainerLogs(getId(), Contents.empty()); } public boolean isRunning() { diff --git a/java/src/org/openqa/selenium/docker/ContainerLogs.java b/java/src/org/openqa/selenium/docker/ContainerLogs.java index 6a66837579e52..2e099c19e6e16 100644 --- a/java/src/org/openqa/selenium/docker/ContainerLogs.java +++ b/java/src/org/openqa/selenium/docker/ContainerLogs.java @@ -17,21 +17,39 @@ package org.openqa.selenium.docker; +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UncheckedIOException; import java.util.List; +import java.util.stream.Collectors; import org.openqa.selenium.internal.Require; +import org.openqa.selenium.remote.http.Contents; public class ContainerLogs { - private final List logLines; + private final Contents.Supplier contents; private final ContainerId id; - public ContainerLogs(ContainerId id, List logLines) { - this.logLines = Require.nonNull("Container logs", logLines); + public ContainerLogs(ContainerId id, Contents.Supplier contents) { + this.contents = Require.nonNull("Container logs", contents); this.id = Require.nonNull("Container id", id); } + /** + * @deprecated List of container logs might be very long. If you need to write down the logs, use + * {@link #getLogs()} to avoid reading the whole content to memory. + */ + @Deprecated public List getLogLines() { - return logLines; + try (BufferedReader in = new BufferedReader(new InputStreamReader(contents.get(), UTF_8))) { + return in.lines().collect(Collectors.toList()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } public ContainerId getId() { @@ -40,6 +58,10 @@ public ContainerId getId() { @Override public String toString() { - return String.format("ContainerInfo{containerLogs=%s, id=%s}", logLines, id); + return String.format("ContainerInfo{id=%s,size=%s}", id, contents.length()); + } + + public InputStream getLogs() { + return contents.get(); } } diff --git a/java/src/org/openqa/selenium/docker/client/GetContainerLogs.java b/java/src/org/openqa/selenium/docker/client/GetContainerLogs.java index ecb59595b6eae..84c74fb94fbcd 100644 --- a/java/src/org/openqa/selenium/docker/client/GetContainerLogs.java +++ b/java/src/org/openqa/selenium/docker/client/GetContainerLogs.java @@ -20,12 +20,10 @@ import static java.net.HttpURLConnection.HTTP_OK; import static org.openqa.selenium.remote.http.HttpMethod.GET; -import java.util.List; import java.util.logging.Logger; import org.openqa.selenium.docker.ContainerId; import org.openqa.selenium.docker.ContainerLogs; import org.openqa.selenium.internal.Require; -import org.openqa.selenium.remote.http.Contents; import org.openqa.selenium.remote.http.HttpHandler; import org.openqa.selenium.remote.http.HttpRequest; import org.openqa.selenium.remote.http.HttpResponse; @@ -51,7 +49,7 @@ public ContainerLogs apply(ContainerId id) { if (res.getStatus() != HTTP_OK) { LOG.warning(() -> "Unable to inspect container " + id); } - List logLines = List.of(Contents.string(res).split("\n")); - return new ContainerLogs(id, logLines); + + return new ContainerLogs(id, res.getContent()); } } diff --git a/java/src/org/openqa/selenium/grid/node/docker/DockerSession.java b/java/src/org/openqa/selenium/grid/node/docker/DockerSession.java index ddc2cfe1ee8bd..650c92789ab81 100644 --- a/java/src/org/openqa/selenium/grid/node/docker/DockerSession.java +++ b/java/src/org/openqa/selenium/grid/node/docker/DockerSession.java @@ -19,17 +19,21 @@ import static java.util.logging.Level.FINE; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Paths; import java.time.Duration; import java.time.Instant; -import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import org.jspecify.annotations.Nullable; import org.openqa.selenium.Capabilities; import org.openqa.selenium.docker.Container; +import org.openqa.selenium.docker.ContainerLogs; import org.openqa.selenium.grid.node.DefaultActiveSession; import org.openqa.selenium.internal.Require; import org.openqa.selenium.remote.Dialect; @@ -81,10 +85,28 @@ private void saveLogs() { } String sessionAssetsPath = assetsPath.getContainerPath(getId()); - String seleniumServerLog = String.format("%s/selenium-server.log", sessionAssetsPath); - try { - List logs = container.getLogs().getLogLines(); - Files.write(Paths.get(seleniumServerLog), logs); + File seleniumServerLog = new File(sessionAssetsPath, "selenium-server.log"); + ContainerLogs containerLogs = container.getLogs(); + + try (InputStream in = new BufferedInputStream(containerLogs.getLogs())) { + // We could just use method `Files.copy(in, seleniumServerLog)`, but it can fail on Java 11. + // See JDK bug https://bugs.openjdk.org/browse/JDK-8228970 + + try (OutputStream out = new BufferedOutputStream(new FileOutputStream(seleniumServerLog))) { + byte[] buffer = new byte[1024]; + + int readCount; + while ((readCount = in.read(buffer)) != -1) { + out.write(buffer, 0, readCount); + } + } + + LOG.log( + FINE, + () -> + String.format( + "Saved container %s logs to file %s", + container.getId(), seleniumServerLog.getAbsolutePath())); } catch (Exception e) { LOG.log(Level.WARNING, "Error saving logs", e); } diff --git a/java/src/org/openqa/selenium/remote/http/jdk/JdkHttpMessages.java b/java/src/org/openqa/selenium/remote/http/jdk/JdkHttpMessages.java index 921f746874ccc..21fafceac7af8 100644 --- a/java/src/org/openqa/selenium/remote/http/jdk/JdkHttpMessages.java +++ b/java/src/org/openqa/selenium/remote/http/jdk/JdkHttpMessages.java @@ -181,7 +181,7 @@ private Contents.Supplier extractContent(java.net.http.HttpResponse response .headers() .firstValue("Content-Type") - .map(contentType -> contentType.equalsIgnoreCase(MediaType.OCTET_STREAM.toString())) + .map(contentType -> isBinaryStream(contentType)) .orElse(false); if (isBinaryStream) { @@ -193,6 +193,12 @@ private Contents.Supplier extractContent(java.net.http.HttpResponse } } + private static boolean isBinaryStream(String contentType) { + return MediaType.OCTET_STREAM.toString().equalsIgnoreCase(contentType) + || "application/vnd.docker.multiplexed-stream".equalsIgnoreCase(contentType) + || "application/vnd.docker.raw-stream".equalsIgnoreCase(contentType); + } + private byte[] readResponseBody(java.net.http.HttpResponse response) { try (InputStream in = response.body()) { return Read.toByteArray(in); From 9ff1136394a5664bf3ff5429cc566fe578c666a9 Mon Sep 17 00:00:00 2001 From: Andrei Solntsev Date: Fri, 13 Mar 2026 11:35:54 +0200 Subject: [PATCH 4/5] #4096 fix NPE when env var "TZ" is missing --- .../openqa/selenium/grid/node/docker/DockerSessionFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/src/org/openqa/selenium/grid/node/docker/DockerSessionFactory.java b/java/src/org/openqa/selenium/grid/node/docker/DockerSessionFactory.java index 189275b166240..af81f42b675c0 100644 --- a/java/src/org/openqa/selenium/grid/node/docker/DockerSessionFactory.java +++ b/java/src/org/openqa/selenium/grid/node/docker/DockerSessionFactory.java @@ -466,7 +466,7 @@ private TimeZone getTimeZone(Capabilities sessionRequestCapabilities) { } } String envTz = System.getenv("TZ"); - if (List.of(TimeZone.getAvailableIDs()).contains(envTz)) { + if (envTz != null && List.of(TimeZone.getAvailableIDs()).contains(envTz)) { return TimeZone.getTimeZone(envTz); } return null; From 9ff3dde6dc5d5b5f59df2bfb1ba463fed50aae3f Mon Sep 17 00:00:00 2001 From: Andrei Solntsev Date: Fri, 13 Mar 2026 12:16:26 +0200 Subject: [PATCH 5/5] #17209 correctly read Docker "/logs" response This response has content type "application/vnd.docker.raw-stream". It's almost a plain text, but we need to skip few "technical" bytes in the beginning of every log line. This is how log lines in the resulting file "selenium-server.log" look like: * Before: `??????g2026-03-13 09:30:55,398 INFO Included extra file` * After: `2026-03-13 09:54:06,477 INFO Included extra file ` --- .../grid/node/docker/DockerSession.java | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/java/src/org/openqa/selenium/grid/node/docker/DockerSession.java b/java/src/org/openqa/selenium/grid/node/docker/DockerSession.java index 650c92789ab81..f7a554c4849c0 100644 --- a/java/src/org/openqa/selenium/grid/node/docker/DockerSession.java +++ b/java/src/org/openqa/selenium/grid/node/docker/DockerSession.java @@ -21,8 +21,11 @@ import java.io.BufferedInputStream; import java.io.BufferedOutputStream; +import java.io.DataInputStream; +import java.io.EOFException; import java.io.File; import java.io.FileOutputStream; +import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URL; @@ -88,27 +91,31 @@ private void saveLogs() { File seleniumServerLog = new File(sessionAssetsPath, "selenium-server.log"); ContainerLogs containerLogs = container.getLogs(); - try (InputStream in = new BufferedInputStream(containerLogs.getLogs())) { - // We could just use method `Files.copy(in, seleniumServerLog)`, but it can fail on Java 11. - // See JDK bug https://bugs.openjdk.org/browse/JDK-8228970 - - try (OutputStream out = new BufferedOutputStream(new FileOutputStream(seleniumServerLog))) { - byte[] buffer = new byte[1024]; - - int readCount; - while ((readCount = in.read(buffer)) != -1) { - out.write(buffer, 0, readCount); - } - } - + try (OutputStream out = new BufferedOutputStream(new FileOutputStream(seleniumServerLog))) { + parseMultiplexedStream(containerLogs.getLogs(), out); LOG.log( FINE, () -> String.format( - "Saved container %s logs to file %s", - container.getId(), seleniumServerLog.getAbsolutePath())); - } catch (Exception e) { + "Saved container %s logs to file %s", container.getId(), seleniumServerLog)); + } catch (IOException e) { LOG.log(Level.WARNING, "Error saving logs", e); } } + + @SuppressWarnings("InfiniteLoopStatement") + private void parseMultiplexedStream(InputStream stream, OutputStream out) throws IOException { + try (DataInputStream in = new DataInputStream(new BufferedInputStream(stream))) { + while (true) { + in.skipBytes(1); // Skip "stream type" byte (1 = stdout, 2 = stderr) + in.skipBytes(3); // Skip the 3 empty padding bytes + int payloadSize = in.readInt(); // Read the 4-byte payload size + byte[] payload = new byte[payloadSize]; + in.readFully(payload); + out.write(payload); + } + } catch (EOFException done) { + LOG.log(FINE, () -> "Finished reading multiplexed stream: " + done); + } + } }