Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions client/src/main/java/org/asynchttpclient/DefaultRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
import java.util.Map;

import static org.asynchttpclient.util.MiscUtils.isNonEmpty;
import static org.asynchttpclient.util.SensitiveLoggingUtils.REDACTED;
import static org.asynchttpclient.util.SensitiveLoggingUtils.isSensitiveHeader;

public class DefaultRequest implements Request {

Expand Down Expand Up @@ -292,8 +294,8 @@ public String toString() {
for (Map.Entry<String, String> header : headers) {
sb.append('\t');
sb.append(header.getKey());
sb.append(':');
sb.append(header.getValue());
sb.append(": ");
sb.append(isSensitiveHeader(header.getKey()) ? REDACTED : header.getValue());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,7 @@ public String toString() {
",\n\tisDone=" + isDone + //
",\n\tisCancelled=" + isCancelled + //
",\n\tasyncHandler=" + asyncHandler + //
// NettyRequest deliberately keeps Object.toString() so request headers are not rendered here.
",\n\tnettyRequest=" + nettyRequest + //
",\n\tfuture=" + future + //
",\n\turi=" + getUri() + //
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ private static boolean abortAfterHandlingHeaders(AsyncHandler<?> handler, HttpHe

private void handleHttpResponse(final HttpResponse response, final Channel channel, final NettyResponseFuture<?> future, AsyncHandler<?> handler) throws Exception {
HttpRequest httpRequest = future.getNettyRequest().getHttpRequest();
logger.debug("\n\nRequest {}\n\nResponse {}\n", httpRequest, response);
if (logger.isDebugEnabled()) {
logger.debug("\n\nRequest {}\n\nResponse {}\n", HttpMessageFormatter.format(httpRequest), HttpMessageFormatter.format(response));
}

future.setKeepAlive(config.getKeepAliveStrategy().keepAlive((InetSocketAddress) channel.remoteAddress(), future.getTargetRequest(), httpRequest, response));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright (c) 2026 AsyncHttpClient Project. All rights reserved.
*
* Licensed 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.asynchttpclient.netty.handler;

import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import org.asynchttpclient.util.SensitiveLoggingUtils;

import java.util.Map;

/**
* Formats HTTP messages for logging. Sensitive header values are redacted unless the
* {@code org.asynchttpclient.enableSensitiveLogging} system property or {@code AHC_ENABLE_SENSITIVE_LOGGING} environment
* variable is set to {@code true} before the sensitive logging policy is initialized. Enabling sensitive logging may
* expose credentials and session data.
*/
public final class HttpMessageFormatter {

/** The value used in place of sensitive header values. */
public static final String REDACTED = SensitiveLoggingUtils.REDACTED;

private HttpMessageFormatter() {
}

static String format(HttpRequest request) {
StringBuilder value = new StringBuilder()
.append(request.method()).append(' ')
.append(request.uri()).append(' ')
.append(request.protocolVersion());
return appendHeaders(value, request.headers()).toString();
}

static String format(HttpResponse response) {
StringBuilder value = new StringBuilder()
.append(response.protocolVersion()).append(' ')
.append(response.status());
return appendHeaders(value, response.headers()).toString();
}

private static StringBuilder appendHeaders(StringBuilder value, HttpHeaders headers) {
for (Map.Entry<String, String> header : headers) {
value.append('\n').append(header.getKey()).append(": ")
.append(isSensitiveHeader(header.getKey()) ? REDACTED : header.getValue());
}
return value;
}

/**
* Returns whether a header value must be redacted from logs.
*
* @param name the header name
* @return {@code true} for authentication and cookie headers when sensitive logging is disabled
*/
public static boolean isSensitiveHeader(CharSequence name) {
return SensitiveLoggingUtils.isSensitiveHeader(name);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public void handleRead(Channel channel, NettyResponseFuture<?> future, Object e)
HttpResponse response = (HttpResponse) e;
if (logger.isDebugEnabled()) {
HttpRequest httpRequest = future.getNettyRequest().getHttpRequest();
logger.debug("\n\nRequest {}\n\nResponse {}\n", httpRequest, response);
logger.debug("\n\nRequest {}\n\nResponse {}\n", HttpMessageFormatter.format(httpRequest), HttpMessageFormatter.format(response));
}

WebSocketUpgradeHandler handler = getWebSocketUpgradeHandler(future);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1105,7 +1105,8 @@ public boolean retry(NettyResponseFuture<?> future) {
if (future.isReplayPossible()) {
future.setChannelState(ChannelState.RECONNECTED);

LOGGER.debug("Trying to recover request {}\n", future.getNettyRequest().getHttpRequest());
HttpRequest request = future.getNettyRequest().getHttpRequest();
LOGGER.debug("Trying to recover request '{}' to '{}'\n", request.method(), request.uri());
try {
future.getAsyncHandler().onRetry();
} catch (Exception e) {
Expand Down Expand Up @@ -1413,7 +1414,7 @@ public void replayRequest(final NettyResponseFuture<?> future, FilterContext fc,
future.setChannelState(ChannelState.NEW);
future.touch();

LOGGER.debug("\n\nReplaying Request {}\n for Future {}\n", newRequest, future);
LOGGER.debug("\n\nReplaying request '{}' to '{}'\n for Future {}\n", newRequest.getMethod(), newRequest.getUri(), future);
try {
future.getAsyncHandler().onRetry();
} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright (c) 2026 AsyncHttpClient Project. All rights reserved.
*
* Licensed 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.asynchttpclient.util;

import io.netty.handler.codec.http.HttpHeaderNames;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Shared policy for rendering sensitive HTTP headers in logs. The policy is read once from the
* {@code org.asynchttpclient.enableSensitiveLogging} system property or {@code AHC_ENABLE_SENSITIVE_LOGGING} environment
* variable when this class is initialized.
*/
public final class SensitiveLoggingUtils {

private static final Logger LOGGER = LoggerFactory.getLogger(SensitiveLoggingUtils.class);
private static final String ENABLE_SENSITIVE_LOGGING_PROPERTY = "org.asynchttpclient.enableSensitiveLogging";
private static final String ENABLE_SENSITIVE_LOGGING_ENVIRONMENT_VARIABLE = "AHC_ENABLE_SENSITIVE_LOGGING";
private static final boolean SENSITIVE_LOGGING_ENABLED = isSensitiveLoggingEnabled();

/** The value used in place of sensitive header values. */
public static final String REDACTED = "<redacted>";

static {
if (SENSITIVE_LOGGING_ENABLED) {
LOGGER.warn("Sensitive HTTP header logging is enabled; credentials and session data may be written to logs");
}
}

private SensitiveLoggingUtils() {
}

/**
* Returns whether a header value must be redacted from logs.
*
* @param name the header name
* @return {@code true} for authentication and cookie headers when sensitive logging is disabled
*/
public static boolean isSensitiveHeader(CharSequence name) {
return !SENSITIVE_LOGGING_ENABLED && (HttpHeaderNames.AUTHORIZATION.contentEqualsIgnoreCase(name)
|| HttpHeaderNames.PROXY_AUTHORIZATION.contentEqualsIgnoreCase(name)
|| HttpHeaderNames.COOKIE.contentEqualsIgnoreCase(name)
|| HttpHeaderNames.SET_COOKIE.contentEqualsIgnoreCase(name));
}

private static boolean isSensitiveLoggingEnabled() {
String propertyValue = System.getProperty(ENABLE_SENSITIVE_LOGGING_PROPERTY);
String value = propertyValue != null ? propertyValue : System.getenv(ENABLE_SENSITIVE_LOGGING_ENVIRONMENT_VARIABLE);
return Boolean.parseBoolean(value);
}
}
20 changes: 20 additions & 0 deletions client/src/test/java/org/asynchttpclient/RequestBuilderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http.cookie.DefaultCookie;
import org.junit.jupiter.api.Test;

import java.util.Collection;
import java.util.Collections;
Expand Down Expand Up @@ -412,4 +413,23 @@ public void testAddHeaderWithIterableShouldLockContentType() {
String contentType = request.getHeaders().get("Content-Type");
assertEquals("text/plain", contentType, "Content-Type set via addHeader(Iterable) should not be modified");
}

@Test
public void testRequestToStringRedactsSensitiveHeaders() {
Request request = get("http://localhost/test")
.setHeader("Authorization", "Bearer request-secret")
.setHeader("Proxy-Authorization", "Basic proxy-secret")
.setHeader("Cookie", "session=cookie-secret")
.setHeader("X-Request-Id", "request-id")
.build();

String value = request.toString();
assertFalse(value.contains("request-secret"));
assertFalse(value.contains("proxy-secret"));
assertFalse(value.contains("cookie-secret"));
assertTrue(value.contains("Authorization: <redacted>"));
assertTrue(value.contains("Proxy-Authorization: <redacted>"));
assertTrue(value.contains("Cookie: <redacted>"));
assertTrue(value.contains("request-id"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* Copyright (c) 2026 AsyncHttpClient Project. All rights reserved.
*
* Licensed 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.asynchttpclient.netty.handler;

import io.netty.handler.codec.http.DefaultHttpRequest;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class HttpMessageFormatterTest {

@Test
public void shouldRedactSensitiveRequestHeaders() {
HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/test");
request.headers()
.set("Authorization", "Bearer request-secret")
.set("proxy-authorization", "Basic proxy-secret")
.set("Cookie", "session=cookie-secret")
.set("X-Request-Id", "request-id");

String value = HttpMessageFormatter.format(request);

assertFalse(value.contains("request-secret"));
assertFalse(value.contains("proxy-secret"));
assertFalse(value.contains("cookie-secret"));
assertTrue(value.contains("Authorization: <redacted>"));
assertTrue(value.contains("proxy-authorization: <redacted>"));
assertTrue(value.contains("X-Request-Id: request-id"));
}

@Test
public void shouldRedactSensitiveResponseHeaders() {
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
response.headers()
.set("Set-Cookie", "session=response-secret")
.set("X-Request-Id", "request-id");

String value = HttpMessageFormatter.format(response);

assertFalse(value.contains("response-secret"));
assertTrue(value.contains("Set-Cookie: <redacted>"));
assertTrue(value.contains("X-Request-Id: request-id"));
}

@Test
public void shouldIncludeSensitiveHeadersWhenSystemPropertyEnabled() throws Exception {
String output = runProbe("true", null);

assertTrue(output.contains("Authorization: Bearer request-secret"), output);
assertTrue(output.contains("Sensitive HTTP header logging is enabled"), output);
}

@Test
public void shouldIncludeSensitiveHeadersWhenEnvironmentVariableEnabled() throws Exception {
String output = runProbe(null, "true");

assertTrue(output.contains("Authorization: Bearer request-secret"), output);
assertTrue(output.contains("Sensitive HTTP header logging is enabled"), output);
}

@Test
public void systemPropertyShouldOverrideEnvironmentVariable() throws Exception {
String output = runProbe("false", "true");

assertTrue(output.contains("Authorization: <redacted>"), output);
assertFalse(output.contains("Sensitive HTTP header logging is enabled"), output);
}

private static String runProbe(String propertyValue, String environmentValue) throws Exception {
List<String> command = new ArrayList<>();
command.add(javaExecutable().toString());
if (propertyValue != null) {
command.add("-Dorg.asynchttpclient.enableSensitiveLogging=" + propertyValue);
}
command.add("-cp");
command.add(System.getProperty("surefire.test.class.path", System.getProperty("java.class.path")));
command.add(SensitiveLoggingProbe.class.getName());

ProcessBuilder processBuilder = new ProcessBuilder(command).redirectErrorStream(true);
if (environmentValue == null) {
processBuilder.environment().remove("AHC_ENABLE_SENSITIVE_LOGGING");
} else {
processBuilder.environment().put("AHC_ENABLE_SENSITIVE_LOGGING", environmentValue);
}

Process process = processBuilder.start();
if (!process.waitFor(30, TimeUnit.SECONDS)) {
process.destroyForcibly();
throw new AssertionError("Sensitive logging probe timed out");
}
String output = new String(process.getInputStream().readAllBytes(), UTF_8);
assertEquals(0, process.exitValue(), output);
return output;
}

private static Path javaExecutable() throws IOException {
String executable = System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("win")
? "java.exe"
: "java";
Path path = Paths.get(System.getProperty("java.home"), "bin", executable);
return path.toRealPath();
}
}
Loading
Loading