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
4 changes: 3 additions & 1 deletion client/src/main/java/org/asynchttpclient/DefaultRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
import java.util.List;
import java.util.Map;

import static org.asynchttpclient.netty.handler.HttpMessageFormatter.REDACTED;
import static org.asynchttpclient.netty.handler.HttpMessageFormatter.isSensitiveHeader;
import static org.asynchttpclient.util.MiscUtils.isNonEmpty;

public class DefaultRequest implements Request {
Expand Down Expand Up @@ -293,7 +295,7 @@ public String toString() {
sb.append('\t');
sb.append(header.getKey());
sb.append(':');
sb.append(header.getValue());
sb.append(isSensitiveHeader(header.getKey()) ? REDACTED : header.getValue());
}
}

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,84 @@
/*
* 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.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;

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} when this class is initialized. Enabling sensitive logging may expose credentials and
* session data.
*/
public final class HttpMessageFormatter {

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>";

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;
}

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);
}

/**
* 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));
}
}
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
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,89 @@
/*
* 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 org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;

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
@EnabledIfSystemProperty(named = "org.asynchttpclient.enableSensitiveLogging", matches = "(?i)true")
public void shouldIncludeSensitiveHeadersWhenSystemPropertyEnabled() {
assertSensitiveHeadersIncluded();
}

@Test
@EnabledIfEnvironmentVariable(named = "AHC_ENABLE_SENSITIVE_LOGGING", matches = "(?i)true")
public void shouldIncludeSensitiveHeadersWhenEnvironmentVariableEnabled() {
assertSensitiveHeadersIncluded();
}

private static void assertSensitiveHeadersIncluded() {
HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/test");
request.headers().set("Authorization", "Bearer request-secret");

String value = HttpMessageFormatter.format(request);

assertFalse(HttpMessageFormatter.isSensitiveHeader("Authorization"));
assertTrue(value.contains("Authorization: Bearer request-secret"));
assertFalse(value.contains(HttpMessageFormatter.REDACTED));
}
}
Loading