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
Original file line number Diff line number Diff line change
Expand Up @@ -336,8 +336,9 @@ private static String requestUri(Uri uri, ProxyServer proxyServer, boolean conne
return uri.getAuthority();

} else if (proxyServer != null && !uri.isSecured() && proxyServer.getProxyType().isHttp()) {
// proxy over HTTP, need full url
return uri.toUrl();
// proxy over HTTP, need full url, minus the userinfo: this request line is sent to the proxy in
// the clear and RFC 9110 §4.2.4 forbids userinfo in a generated request target
return uri.toUrlWithoutUserInfo();

} else {
// direct connection to target host or tunnel already connected: only path and query
Expand Down
28 changes: 28 additions & 0 deletions client/src/main/java/org/asynchttpclient/uri/Uri.java
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,36 @@
return url;
}

/**
* Same as {@link #toUrl()} but without the deprecated userinfo subcomponent. RFC 9110 §4.2.4: a sender
* MUST NOT generate the userinfo subcomponent (and its "@" delimiter) when an origin-form or
* absolute-form URI reference is generated for a request target, so this is what belongs on the wire
* when an absolute-form target is required. {@link #toUrl()} keeps the userinfo for the caller-visible
* URL.
*
* @return [scheme]://[hostname](:[port])/path(?[query])
*/
public String toUrlWithoutUserInfo() {
if (userInfo == null) {
return toUrl();
}

StringBuilder sb = StringBuilderPool.DEFAULT.stringBuilder();
sb.append(scheme).append("://").append(host);
if (port != -1) {
sb.append(':').append(port);
}
if (path != null) {
sb.append(path);
}
if (query != null) {
sb.append('?').append(query);
}
return sb.toString();
}

/**
* @return [scheme]://[hostname](:[port])/path. Port is omitted if it matches the scheme's default one.

Check warning on line 177 in client/src/main/java/org/asynchttpclient/uri/Uri.java

View workflow job for this annotation

GitHub Actions / compile-and-check

[MissingSummary] A summary fragment is required; consider using the value of the @return block as a summary fragment instead.

Check warning on line 177 in client/src/main/java/org/asynchttpclient/uri/Uri.java

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 21)

[MissingSummary] A summary fragment is required; consider using the value of the @return block as a summary fragment instead.

Check warning on line 177 in client/src/main/java/org/asynchttpclient/uri/Uri.java

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 25)

[MissingSummary] A summary fragment is required; consider using the value of the @return block as a summary fragment instead.

Check warning on line 177 in client/src/main/java/org/asynchttpclient/uri/Uri.java

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 17)

[MissingSummary] A summary fragment is required; consider using the value of the @return block as a summary fragment instead.

Check warning on line 177 in client/src/main/java/org/asynchttpclient/uri/Uri.java

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 11)

[MissingSummary] A summary fragment is required; consider using the value of the @return block as a summary fragment instead.

Check warning on line 177 in client/src/main/java/org/asynchttpclient/uri/Uri.java

View workflow job for this annotation

GitHub Actions / test (macos-latest, 25)

[MissingSummary] A summary fragment is required; consider using the value of the @return block as a summary fragment instead.

Check warning on line 177 in client/src/main/java/org/asynchttpclient/uri/Uri.java

View workflow job for this annotation

GitHub Actions / test (macos-latest, 17)

[MissingSummary] A summary fragment is required; consider using the value of the @return block as a summary fragment instead.

Check warning on line 177 in client/src/main/java/org/asynchttpclient/uri/Uri.java

View workflow job for this annotation

GitHub Actions / test (macos-latest, 11)

[MissingSummary] A summary fragment is required; consider using the value of the @return block as a summary fragment instead.

Check warning on line 177 in client/src/main/java/org/asynchttpclient/uri/Uri.java

View workflow job for this annotation

GitHub Actions / test (macos-latest, 21)

[MissingSummary] A summary fragment is required; consider using the value of the @return block as a summary fragment instead.
*/
public String toBaseUrl() {
StringBuilder sb = StringBuilderPool.DEFAULT.stringBuilder();
Expand Down Expand Up @@ -225,7 +253,7 @@
}

@Override
public boolean equals(Object obj) {

Check warning on line 256 in client/src/main/java/org/asynchttpclient/uri/Uri.java

View workflow job for this annotation

GitHub Actions / compile-and-check

[EqualsGetClass] Prefer instanceof to getClass when implementing Object#equals.

Check warning on line 256 in client/src/main/java/org/asynchttpclient/uri/Uri.java

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 17)

[EqualsGetClass] Prefer instanceof to getClass when implementing Object#equals.

Check warning on line 256 in client/src/main/java/org/asynchttpclient/uri/Uri.java

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 11)

[EqualsGetClass] Prefer instanceof to getClass when implementing Object#equals.

Check warning on line 256 in client/src/main/java/org/asynchttpclient/uri/Uri.java

View workflow job for this annotation

GitHub Actions / test (macos-latest, 17)

[EqualsGetClass] Prefer instanceof to getClass when implementing Object#equals.

Check warning on line 256 in client/src/main/java/org/asynchttpclient/uri/Uri.java

View workflow job for this annotation

GitHub Actions / test (macos-latest, 11)

[EqualsGetClass] Prefer instanceof to getClass when implementing Object#equals.
if (this == obj) {
return true;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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.request;

import org.asynchttpclient.Request;
import org.asynchttpclient.proxy.ProxyServer;
import org.junit.jupiter.api.Test;

import static org.asynchttpclient.Dsl.config;
import static org.asynchttpclient.Dsl.get;
import static org.asynchttpclient.Dsl.proxyServer;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;

/**
* A plaintext request routed through an HTTP proxy carries an absolute-form request target, which the proxy
* sees and logs in the clear. RFC 9110 §4.2.4 forbids the userinfo subcomponent in a generated request
* target, so the URL credentials must not appear on that request line.
*/
public class ProxyRequestUriUserInfoTest {

private static NettyRequestFactory factory() {
return new NettyRequestFactory(config().build());
}

@Test
public void proxiedRequestTargetDropsUserInfo() {
Request request = get("http://user:secret@origin.example.com/resource?a=b").build();
ProxyServer proxy = proxyServer("proxy.example.com", 8080).build();

NettyRequest proxied = factory().newNettyRequest(request, false, proxy, null, null);
String requestTarget = proxied.getHttpRequest().uri();

assertFalse(requestTarget.contains("secret"),
"the absolute-form request target must not expose the URL credentials to the proxy");
assertEquals("http://origin.example.com/resource?a=b", requestTarget);
}

@Test
public void proxiedRequestTargetWithoutUserInfoIsUnchanged() {
Request request = get("http://origin.example.com/resource?a=b").build();
ProxyServer proxy = proxyServer("proxy.example.com", 8080).build();

NettyRequest proxied = factory().newNettyRequest(request, false, proxy, null, null);

assertEquals("http://origin.example.com/resource?a=b", proxied.getHttpRequest().uri());
}

@Test
public void directRequestTargetStaysRelative() {
Request request = get("http://user:secret@origin.example.com/resource?a=b").build();

NettyRequest direct = factory().newNettyRequest(request, false, null, null, null);

assertEquals("/resource?a=b", direct.getHttpRequest().uri());
}
}
Loading