diff --git a/java/client/src/org/openqa/selenium/remote/RemoteWebDriverBuilder.java b/java/client/src/org/openqa/selenium/remote/RemoteWebDriverBuilder.java index 96938fea32aa9..379fa4f8bba7e 100644 --- a/java/client/src/org/openqa/selenium/remote/RemoteWebDriverBuilder.java +++ b/java/client/src/org/openqa/selenium/remote/RemoteWebDriverBuilder.java @@ -24,11 +24,11 @@ import org.openqa.selenium.ImmutableCapabilities; import org.openqa.selenium.SessionNotCreatedException; import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebDriverException; import org.openqa.selenium.WebDriverInfo; import org.openqa.selenium.internal.Either; import org.openqa.selenium.internal.Require; import org.openqa.selenium.remote.http.ClientConfig; +import org.openqa.selenium.remote.http.DumpHttpExchangeFilter; import org.openqa.selenium.remote.http.Filter; import org.openqa.selenium.remote.http.HttpClient; import org.openqa.selenium.remote.http.HttpHandler; @@ -355,7 +355,8 @@ private WebDriver getRemoteDriver() { HttpHandler handler = Require.nonNull("Http handler", client) .with(new CloseHttpClientFilter(client) .andThen(new AddWebDriverSpecHeaders()) - .andThen(new ErrorFilter())); + .andThen(new ErrorFilter()) + .andThen(new DumpHttpExchangeFilter())); Either result = null; try { diff --git a/java/client/src/org/openqa/selenium/remote/http/DumpHttpExchangeFilter.java b/java/client/src/org/openqa/selenium/remote/http/DumpHttpExchangeFilter.java new file mode 100644 index 0000000000000..e9a6e1f50df5c --- /dev/null +++ b/java/client/src/org/openqa/selenium/remote/http/DumpHttpExchangeFilter.java @@ -0,0 +1,91 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.openqa.selenium.remote.http; + +import com.google.common.annotations.VisibleForTesting; +import org.openqa.selenium.internal.Debug; +import org.openqa.selenium.internal.Require; + +import java.io.InputStream; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.StreamSupport; + +import static java.util.stream.Collectors.joining; + +public class DumpHttpExchangeFilter implements Filter { + + public static final Logger LOG = Logger.getLogger(DumpHttpExchangeFilter.class.getName()); + private final Level logLevel; + + public DumpHttpExchangeFilter() { + this(Debug.getDebugLogLevel()); + } + + public DumpHttpExchangeFilter(Level logLevel) { + this.logLevel = Require.nonNull("Log level", logLevel); + } + + @Override + public HttpHandler apply(HttpHandler next) { + return req -> { + // Use the supplier to avoid messing with the request unless we're logging + LOG.log(logLevel, () -> requestLogMessage(req)); + + HttpResponse res = next.execute(req); + + LOG.log(logLevel, () -> responseLogMessage(res)); + + return res; + }; + } + + private void expandHeadersAndContent(StringBuilder builder, HttpMessage message) { + message.getHeaderNames().forEach(name -> { + builder.append(" ").append(name).append(": "); + builder.append(StreamSupport.stream(message.getHeaders(name).spliterator(), false).collect(joining(", "))); + builder.append("\n"); + }); + builder.append("\n"); + builder.append(Contents.string(message)); + } + + @VisibleForTesting + String requestLogMessage(HttpRequest req) { + // There's no requirement that requests or responses can be read more than once. Protect ourselves. + Supplier memoized = Contents.memoize(req.getContent()); + req.setContent(memoized); + + StringBuilder reqInfo = new StringBuilder(); + reqInfo.append("HTTP Request: ").append(req).append("\n"); + expandHeadersAndContent(reqInfo, req); + return reqInfo.toString(); + } + + @VisibleForTesting + String responseLogMessage(HttpResponse res) { + Supplier resContents = Contents.memoize(res.getContent()); + res.setContent(resContents); + + StringBuilder resInfo = new StringBuilder("HTTP Response: "); + resInfo.append("Status code: ").append(res.getStatus()).append("\n"); + expandHeadersAndContent(resInfo, res); + return resInfo.toString(); + } +} diff --git a/java/client/test/org/openqa/selenium/remote/http/BUILD.bazel b/java/client/test/org/openqa/selenium/remote/http/BUILD.bazel index fbe354d275c19..621cf92fc74a6 100644 --- a/java/client/test/org/openqa/selenium/remote/http/BUILD.bazel +++ b/java/client/test/org/openqa/selenium/remote/http/BUILD.bazel @@ -2,7 +2,7 @@ load("@rules_jvm_external//:defs.bzl", "artifact") load("//java:defs.bzl", "java_test_suite") java_test_suite( - name = "SmallTests", + name = "small-tests", size = "small", srcs = glob(["*.java"]), javacopts = [ diff --git a/java/client/test/org/openqa/selenium/remote/http/DumpHttpExchangeFilterTest.java b/java/client/test/org/openqa/selenium/remote/http/DumpHttpExchangeFilterTest.java new file mode 100644 index 0000000000000..c4a29a9214156 --- /dev/null +++ b/java/client/test/org/openqa/selenium/remote/http/DumpHttpExchangeFilterTest.java @@ -0,0 +1,62 @@ +package org.openqa.selenium.remote.http; + +import org.junit.Test; + +import java.nio.charset.StandardCharsets; +import java.util.logging.Level; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.openqa.selenium.remote.http.Contents.string; +import static org.openqa.selenium.remote.http.HttpMethod.GET; + +public class DumpHttpExchangeFilterTest { + + @Test + public void shouldIncludeRequestAndResponseHeaders() { + DumpHttpExchangeFilter dumpFilter = new DumpHttpExchangeFilter(); + + String reqLog = dumpFilter.requestLogMessage( + new HttpRequest(GET, "/foo").addHeader("Peas", "and Sausages")); + + assertThat(reqLog).contains("Peas"); + assertThat(reqLog).contains("and Sausages"); + + String resLog = dumpFilter.responseLogMessage(new HttpResponse() + .addHeader("Cheese", "Brie") + .setContent(string("Hello, World!", UTF_8))); + + assertThat(resLog).contains("Cheese"); + assertThat(resLog).contains("Brie"); + } + + @Test + public void shouldIncludeRequestContentInLogMessage() { + DumpHttpExchangeFilter dumpFilter = new DumpHttpExchangeFilter(); + + String reqLog = dumpFilter.requestLogMessage( + new HttpRequest(GET, "/foo").setContent(Contents.string("Cheese is lovely", UTF_8))); + + assertThat(reqLog).contains("Cheese is lovely"); + } + + @Test + public void shouldIncludeResponseCodeInLogMessage() { + DumpHttpExchangeFilter dumpFilter = new DumpHttpExchangeFilter(); + + String resLog = dumpFilter.responseLogMessage( + new HttpResponse().setStatus(505)); + + assertThat(resLog).contains("505"); + } + + @Test + public void shouldIncludeBodyOfResponseInLogMessage() { + DumpHttpExchangeFilter dumpFilter = new DumpHttpExchangeFilter(); + + String resLog = dumpFilter.responseLogMessage( + new HttpResponse().setContent(Contents.string("Peas", UTF_8))); + + assertThat(resLog).contains("Peas"); + } +}