Skip to content
Merged
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
61 changes: 58 additions & 3 deletions client/src/main/java/org/asynchttpclient/RequestBuilderBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,30 @@ public abstract class RequestBuilderBase<T extends RequestBuilderBase<T>> {
protected @Nullable Charset charset;
protected ChannelPoolPartitioning channelPoolPartitioning = ChannelPoolPartitioning.PerHostChannelPoolPartitioning.INSTANCE;
protected NameResolver<InetAddress> nameResolver = DEFAULT_NAME_RESOLVER;
protected boolean contentTypeLocked;

/**
* Mark the Content-Type header as explicitly set by the user. When locked, the
* Content-Type header will not be modified by the client (e.g., charset addition).
*/
protected final void doContentTypeLock() {
this.contentTypeLocked = true;
}

/**
* Clear the Content-Type lock, allowing the client to modify the Content-Type header
* if needed (for example, to add charset when it was auto-generated).
*/
protected final void resetContentTypeLock() {
this.contentTypeLocked = false;
}

/**
* Return whether the Content-Type header has been locked as explicitly set by the user.
*/
protected final boolean isContentTypeLocked() {
return this.contentTypeLocked;
}

protected RequestBuilderBase(String method, boolean disableUrlEncoding) {
this(method, disableUrlEncoding, true);
Expand All @@ -116,6 +140,10 @@ protected RequestBuilderBase(Request prototype, boolean disableUrlEncoding, bool
localAddress = prototype.getLocalAddress();
headers = new DefaultHttpHeaders(validateHeaders);
headers.add(prototype.getHeaders());
// If prototype has Content-Type, consider it as explicitly set
if (headers.contains(CONTENT_TYPE)) {
doContentTypeLock();
}
if (isNonEmpty(prototype.getCookies())) {
cookies = new ArrayList<>(prototype.getCookies());
}
Expand Down Expand Up @@ -181,6 +209,7 @@ public T setVirtualHost(String virtualHost) {
*/
public T clearHeaders() {
headers.clear();
resetContentTypeLock();
return asDerivedType();
}

Expand All @@ -203,6 +232,9 @@ public T setHeader(CharSequence name, String value) {
*/
public T setHeader(CharSequence name, Object value) {
headers.set(name, value);
if (CONTENT_TYPE.contentEqualsIgnoreCase(name)) {
doContentTypeLock();
}
return asDerivedType();
}

Expand All @@ -215,6 +247,9 @@ public T setHeader(CharSequence name, Object value) {
*/
public T setHeader(CharSequence name, Iterable<?> values) {
headers.set(name, values);
if (CONTENT_TYPE.contentEqualsIgnoreCase(name)) {
doContentTypeLock();
}
return asDerivedType();
}

Expand Down Expand Up @@ -243,6 +278,9 @@ public T addHeader(CharSequence name, Object value) {
}

headers.add(name, value);
if (CONTENT_TYPE.contentEqualsIgnoreCase(name)) {
doContentTypeLock();
}
return asDerivedType();
}

Expand All @@ -256,6 +294,9 @@ public T addHeader(CharSequence name, Object value) {
*/
public T addHeader(CharSequence name, Iterable<?> values) {
headers.add(name, values);
if (CONTENT_TYPE.contentEqualsIgnoreCase(name)) {
doContentTypeLock();
}
return asDerivedType();
}

Expand All @@ -264,6 +305,9 @@ public T setHeaders(HttpHeaders headers) {
this.headers.clear();
} else {
this.headers = headers;
if (headers.contains(CONTENT_TYPE)) {
doContentTypeLock();
}
}
return asDerivedType();
}
Expand All @@ -278,7 +322,12 @@ public T setHeaders(HttpHeaders headers) {
public T setHeaders(Map<? extends CharSequence, ? extends Iterable<?>> headers) {
clearHeaders();
if (headers != null) {
headers.forEach((name, values) -> this.headers.add(name, values));
headers.forEach((name, values) -> {
this.headers.add(name, values);
if (CONTENT_TYPE.contentEqualsIgnoreCase(name)) {
doContentTypeLock();
}
});
}
return asDerivedType();
}
Expand All @@ -293,7 +342,12 @@ public T setHeaders(Map<? extends CharSequence, ? extends Iterable<?>> headers)
public T setSingleHeaders(Map<? extends CharSequence, ?> headers) {
clearHeaders();
if (headers != null) {
headers.forEach((name, value) -> this.headers.add(name, value));
headers.forEach((name, value) -> {
this.headers.add(name, value);
if (CONTENT_TYPE.contentEqualsIgnoreCase(name)) {
doContentTypeLock();
}
});
}
return asDerivedType();
}
Expand Down Expand Up @@ -634,7 +688,8 @@ private void updateCharset() {
String contentTypeHeader = headers.get(CONTENT_TYPE);
Charset contentTypeCharset = extractContentTypeCharsetAttribute(contentTypeHeader);
charset = withDefault(contentTypeCharset, withDefault(charset, UTF_8));
if (contentTypeHeader != null && contentTypeHeader.regionMatches(true, 0, "text/", 0, 5) && contentTypeCharset == null) {
// Only add charset if Content-Type was not explicitly set by user
if (!isContentTypeLocked() && contentTypeHeader != null && contentTypeHeader.regionMatches(true, 0, "text/", 0, 5) && contentTypeCharset == null) {
// add explicit charset to content-type header
headers.set(CONTENT_TYPE, contentTypeHeader + "; charset=" + charset.name());
}
Expand Down
192 changes: 192 additions & 0 deletions client/src/test/java/org/asynchttpclient/RequestBuilderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
package org.asynchttpclient;

import io.github.artsok.RepeatedIfExceptionsTest;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http.cookie.DefaultCookie;
Expand All @@ -29,7 +31,9 @@
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.singletonList;
import static org.asynchttpclient.Dsl.get;
import static org.asynchttpclient.Dsl.post;
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 RequestBuilderTest {
Expand Down Expand Up @@ -220,4 +224,192 @@ public void testSettingHeadersUsingMapWithStringKeys() {
Request request = requestBuilder.build();
assertEquals(request.getHeaders().get("X-Forwarded-For"), "10.0.0.1");
}

@RepeatedIfExceptionsTest(repeats = 5)
public void testUserSetTextPlainContentTypeShouldNotBeModified() {
Request request = post("http://localhost/test")
.setHeader("Content-Type", "text/plain")
.setBody("Hello World")
.build();

String contentType = request.getHeaders().get("Content-Type");
assertEquals("text/plain", contentType, "Content-Type should not be modified when user explicitly sets it");
assertFalse(contentType.contains("charset"), "Charset should not be added to user-specified Content-Type");
}

@RepeatedIfExceptionsTest(repeats = 5)
public void testUserSetTextXmlContentTypeShouldNotBeModified() {
Request request = post("http://localhost/test")
.setHeader("Content-Type", "text/xml")
.setBody("<test>Hello</test>")
.build();

String contentType = request.getHeaders().get("Content-Type");
assertEquals("text/xml", contentType, "Content-Type should not be modified when user explicitly sets it");
}

@RepeatedIfExceptionsTest(repeats = 5)
public void testUserSetTextHtmlContentTypeShouldNotBeModified() {
Request request = post("http://localhost/test")
.setHeader("Content-Type", "text/html")
.setBody("<html></html>")
.build();

String contentType = request.getHeaders().get("Content-Type");
assertEquals("text/html", contentType, "Content-Type should not be modified when user explicitly sets it");
}

@RepeatedIfExceptionsTest(repeats = 5)
public void testUserSetContentTypeWithCharsetShouldBePreserved() {
Request request = post("http://localhost/test")
.setHeader("Content-Type", "text/xml; charset=ISO-8859-1")
.setBody("<test>Hello</test>")
.build();

String contentType = request.getHeaders().get("Content-Type");
assertEquals("text/xml; charset=ISO-8859-1", contentType, "User-specified charset should be preserved");
assertTrue(contentType.contains("ISO-8859-1"), "ISO-8859-1 charset should be preserved");
assertFalse(contentType.contains("UTF-8"), "UTF-8 should not be added");
}

@RepeatedIfExceptionsTest(repeats = 5)
public void testApplicationJsonContentTypeShouldNotBeModified() {
Request request = post("http://localhost/test")
.setHeader("Content-Type", "application/json")
.setBody("{\"key\": \"value\"}")
.build();

String contentType = request.getHeaders().get("Content-Type");
assertEquals("application/json", contentType, "application/json should not be modified");
assertFalse(contentType.contains("charset"), "Charset should not be added to application/json");
}

@RepeatedIfExceptionsTest(repeats = 5)
public void testAddHeaderContentTypeShouldNotBeModified() {
Request request = post("http://localhost/test")
.addHeader("Content-Type", "text/plain")
.setBody("Hello World")
.build();

String contentType = request.getHeaders().get("Content-Type");
assertEquals("text/plain", contentType, "Content-Type set via addHeader should not be modified");
}

@RepeatedIfExceptionsTest(repeats = 5)
public void testSetHeadersWithHttpHeadersShouldLockContentType() {
HttpHeaders httpHeaders = new DefaultHttpHeaders();
httpHeaders.set("Content-Type", "text/plain");

Request request = post("http://localhost/test")
.setHeaders(httpHeaders)
.setBody("Hello World")
.build();

String contentType = request.getHeaders().get("Content-Type");
assertEquals("text/plain", contentType, "Content-Type set via setHeaders(HttpHeaders) should not be modified");
}

@RepeatedIfExceptionsTest(repeats = 5)
public void testSetHeadersWithMapShouldLockContentType() {
Map<String, List<String>> headerMap = new HashMap<>();
headerMap.put("Content-Type", singletonList("text/plain"));

Request request = post("http://localhost/test")
.setHeaders(headerMap)
.setBody("Hello World")
.build();

String contentType = request.getHeaders().get("Content-Type");
assertEquals("text/plain", contentType, "Content-Type set via setHeaders(Map) should not be modified");
}

@RepeatedIfExceptionsTest(repeats = 5)
public void testSetSingleHeadersShouldLockContentType() {
Map<String, String> headerMap = new HashMap<>();
headerMap.put("Content-Type", "text/plain");

Request request = post("http://localhost/test")
.setSingleHeaders(headerMap)
.setBody("Hello World")
.build();

String contentType = request.getHeaders().get("Content-Type");
assertEquals("text/plain", contentType, "Content-Type set via setSingleHeaders should not be modified");
}

@RepeatedIfExceptionsTest(repeats = 5)
public void testClearHeadersShouldResetContentTypeLock() {
Request request = post("http://localhost/test")
.setHeader("Content-Type", "text/plain")
.clearHeaders()
.setHeader("Content-Type", "text/xml")
.setBody("<test></test>")
.build();

String contentType = request.getHeaders().get("Content-Type");
assertEquals("text/xml", contentType, "Content-Type should still be preserved after clear and re-set");
}

@RepeatedIfExceptionsTest(repeats = 5)
public void testPrototypeRequestShouldPreserveContentType() {
Request original = post("http://localhost/test")
.setHeader("Content-Type", "text/plain")
.setBody("Hello")
.build();

Request copy = post("http://localhost/test")
.setUrl(original.getUri().toUrl())
.setHeaders(original.getHeaders())
.setBody("Hello")
.build();

String contentType = copy.getHeaders().get("Content-Type");
assertEquals("text/plain", contentType, "Content-Type should be preserved from prototype");
}

@RepeatedIfExceptionsTest(repeats = 5)
public void testRequestBuilderFromPrototypeShouldPreserveContentType() {
Request original = post("http://localhost/test")
.setHeader("Content-Type", "text/plain")
.setBody("Hello")
.build();

Request copy = new RequestBuilder(original).build();

String contentType = copy.getHeaders().get("Content-Type");
assertEquals("text/plain", contentType, "Content-Type should be preserved from prototype via RequestBuilder");
}

@RepeatedIfExceptionsTest(repeats = 5)
public void testCaseInsensitiveContentTypeHeader() {
Request request = post("http://localhost/test")
.setHeader("content-type", "text/plain")
.setBody("Hello World")
.build();

String contentType = request.getHeaders().get("Content-Type");
assertEquals("text/plain", contentType, "Content-Type should be matched case-insensitively");
}

@RepeatedIfExceptionsTest(repeats = 5)
public void testSetHeaderWithIterableShouldLockContentType() {
Request request = post("http://localhost/test")
.setHeader("Content-Type", singletonList("text/plain"))
.setBody("Hello World")
.build();

String contentType = request.getHeaders().get("Content-Type");
assertEquals("text/plain", contentType, "Content-Type set via setHeader(Iterable) should not be modified");
}

@RepeatedIfExceptionsTest(repeats = 5)
public void testAddHeaderWithIterableShouldLockContentType() {
Request request = post("http://localhost/test")
.addHeader("Content-Type", singletonList("text/plain"))
.setBody("Hello World")
.build();

String contentType = request.getHeaders().get("Content-Type");
assertEquals("text/plain", contentType, "Content-Type set via addHeader(Iterable) should not be modified");
}
}