Skip to content

Commit d506ef6

Browse files
committed
Maintain Content-Type set explicitly by client
1 parent a177bd3 commit d506ef6

File tree

2 files changed

+251
-3
lines changed

2 files changed

+251
-3
lines changed

client/src/main/java/org/asynchttpclient/RequestBuilderBase.java

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,31 @@ public abstract class RequestBuilderBase<T extends RequestBuilderBase<T>> {
9393
protected @Nullable Charset charset;
9494
protected ChannelPoolPartitioning channelPoolPartitioning = ChannelPoolPartitioning.PerHostChannelPoolPartitioning.INSTANCE;
9595
protected NameResolver<InetAddress> nameResolver = DEFAULT_NAME_RESOLVER;
96+
// Flag to track if Content-Type was explicitly set by user (should not be modified)
97+
private boolean contentTypeLocked;
98+
99+
/**
100+
* Mark the Content-Type header as explicitly set by the user. When locked, the
101+
* Content-Type header will not be modified by the client (e.g., charset addition).
102+
*/
103+
protected final void doContentTypeLock() {
104+
this.contentTypeLocked = true;
105+
}
106+
107+
/**
108+
* Clear the Content-Type lock, allowing the client to modify the Content-Type header
109+
* if needed (for example, to add charset when it was auto-generated).
110+
*/
111+
protected final void resetContentTypeLock() {
112+
this.contentTypeLocked = false;
113+
}
114+
115+
/**
116+
* Return whether the Content-Type header has been locked as explicitly set by the user.
117+
*/
118+
protected final boolean isContentTypeLocked() {
119+
return this.contentTypeLocked;
120+
}
96121

97122
protected RequestBuilderBase(String method, boolean disableUrlEncoding) {
98123
this(method, disableUrlEncoding, true);
@@ -116,6 +141,10 @@ protected RequestBuilderBase(Request prototype, boolean disableUrlEncoding, bool
116141
localAddress = prototype.getLocalAddress();
117142
headers = new DefaultHttpHeaders(validateHeaders);
118143
headers.add(prototype.getHeaders());
144+
// If prototype has Content-Type, consider it as explicitly set
145+
if (headers.contains(CONTENT_TYPE)) {
146+
doContentTypeLock();
147+
}
119148
if (isNonEmpty(prototype.getCookies())) {
120149
cookies = new ArrayList<>(prototype.getCookies());
121150
}
@@ -181,6 +210,7 @@ public T setVirtualHost(String virtualHost) {
181210
*/
182211
public T clearHeaders() {
183212
headers.clear();
213+
resetContentTypeLock();
184214
return asDerivedType();
185215
}
186216

@@ -203,6 +233,9 @@ public T setHeader(CharSequence name, String value) {
203233
*/
204234
public T setHeader(CharSequence name, Object value) {
205235
headers.set(name, value);
236+
if (CONTENT_TYPE.contentEqualsIgnoreCase(name)) {
237+
doContentTypeLock();
238+
}
206239
return asDerivedType();
207240
}
208241

@@ -215,6 +248,9 @@ public T setHeader(CharSequence name, Object value) {
215248
*/
216249
public T setHeader(CharSequence name, Iterable<?> values) {
217250
headers.set(name, values);
251+
if (CONTENT_TYPE.contentEqualsIgnoreCase(name)) {
252+
doContentTypeLock();
253+
}
218254
return asDerivedType();
219255
}
220256

@@ -243,6 +279,9 @@ public T addHeader(CharSequence name, Object value) {
243279
}
244280

245281
headers.add(name, value);
282+
if (CONTENT_TYPE.contentEqualsIgnoreCase(name)) {
283+
doContentTypeLock();
284+
}
246285
return asDerivedType();
247286
}
248287

@@ -256,6 +295,9 @@ public T addHeader(CharSequence name, Object value) {
256295
*/
257296
public T addHeader(CharSequence name, Iterable<?> values) {
258297
headers.add(name, values);
298+
if (CONTENT_TYPE.contentEqualsIgnoreCase(name)) {
299+
doContentTypeLock();
300+
}
259301
return asDerivedType();
260302
}
261303

@@ -264,6 +306,9 @@ public T setHeaders(HttpHeaders headers) {
264306
this.headers.clear();
265307
} else {
266308
this.headers = headers;
309+
if (headers.contains(CONTENT_TYPE)) {
310+
doContentTypeLock();
311+
}
267312
}
268313
return asDerivedType();
269314
}
@@ -278,7 +323,12 @@ public T setHeaders(HttpHeaders headers) {
278323
public T setHeaders(Map<? extends CharSequence, ? extends Iterable<?>> headers) {
279324
clearHeaders();
280325
if (headers != null) {
281-
headers.forEach((name, values) -> this.headers.add(name, values));
326+
headers.forEach((name, values) -> {
327+
this.headers.add(name, values);
328+
if (CONTENT_TYPE.contentEqualsIgnoreCase(name)) {
329+
doContentTypeLock();
330+
}
331+
});
282332
}
283333
return asDerivedType();
284334
}
@@ -293,7 +343,12 @@ public T setHeaders(Map<? extends CharSequence, ? extends Iterable<?>> headers)
293343
public T setSingleHeaders(Map<? extends CharSequence, ?> headers) {
294344
clearHeaders();
295345
if (headers != null) {
296-
headers.forEach((name, value) -> this.headers.add(name, value));
346+
headers.forEach((name, value) -> {
347+
this.headers.add(name, value);
348+
if (CONTENT_TYPE.contentEqualsIgnoreCase(name)) {
349+
doContentTypeLock();
350+
}
351+
});
297352
}
298353
return asDerivedType();
299354
}
@@ -634,7 +689,8 @@ private void updateCharset() {
634689
String contentTypeHeader = headers.get(CONTENT_TYPE);
635690
Charset contentTypeCharset = extractContentTypeCharsetAttribute(contentTypeHeader);
636691
charset = withDefault(contentTypeCharset, withDefault(charset, UTF_8));
637-
if (contentTypeHeader != null && contentTypeHeader.regionMatches(true, 0, "text/", 0, 5) && contentTypeCharset == null) {
692+
// Only add charset if Content-Type was not explicitly set by user
693+
if (!isContentTypeLocked() && contentTypeHeader != null && contentTypeHeader.regionMatches(true, 0, "text/", 0, 5) && contentTypeCharset == null) {
638694
// add explicit charset to content-type header
639695
headers.set(CONTENT_TYPE, contentTypeHeader + "; charset=" + charset.name());
640696
}

client/src/test/java/org/asynchttpclient/RequestBuilderTest.java

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
package org.asynchttpclient;
1717

1818
import io.github.artsok.RepeatedIfExceptionsTest;
19+
import io.netty.handler.codec.http.DefaultHttpHeaders;
20+
import io.netty.handler.codec.http.HttpHeaders;
1921
import io.netty.handler.codec.http.HttpMethod;
2022
import io.netty.handler.codec.http.cookie.Cookie;
2123
import io.netty.handler.codec.http.cookie.DefaultCookie;
@@ -29,7 +31,9 @@
2931
import static java.nio.charset.StandardCharsets.UTF_8;
3032
import static java.util.Collections.singletonList;
3133
import static org.asynchttpclient.Dsl.get;
34+
import static org.asynchttpclient.Dsl.post;
3235
import static org.junit.jupiter.api.Assertions.assertEquals;
36+
import static org.junit.jupiter.api.Assertions.assertFalse;
3337
import static org.junit.jupiter.api.Assertions.assertTrue;
3438

3539
public class RequestBuilderTest {
@@ -220,4 +224,192 @@ public void testSettingHeadersUsingMapWithStringKeys() {
220224
Request request = requestBuilder.build();
221225
assertEquals(request.getHeaders().get("X-Forwarded-For"), "10.0.0.1");
222226
}
227+
228+
@RepeatedIfExceptionsTest(repeats = 5)
229+
public void testUserSetTextPlainContentTypeShouldNotBeModified() {
230+
Request request = post("http://localhost/test")
231+
.setHeader("Content-Type", "text/plain")
232+
.setBody("Hello World")
233+
.build();
234+
235+
String contentType = request.getHeaders().get("Content-Type");
236+
assertEquals("text/plain", contentType, "Content-Type should not be modified when user explicitly sets it");
237+
assertFalse(contentType.contains("charset"), "Charset should not be added to user-specified Content-Type");
238+
}
239+
240+
@RepeatedIfExceptionsTest(repeats = 5)
241+
public void testUserSetTextXmlContentTypeShouldNotBeModified() {
242+
Request request = post("http://localhost/test")
243+
.setHeader("Content-Type", "text/xml")
244+
.setBody("<test>Hello</test>")
245+
.build();
246+
247+
String contentType = request.getHeaders().get("Content-Type");
248+
assertEquals("text/xml", contentType, "Content-Type should not be modified when user explicitly sets it");
249+
}
250+
251+
@RepeatedIfExceptionsTest(repeats = 5)
252+
public void testUserSetTextHtmlContentTypeShouldNotBeModified() {
253+
Request request = post("http://localhost/test")
254+
.setHeader("Content-Type", "text/html")
255+
.setBody("<html></html>")
256+
.build();
257+
258+
String contentType = request.getHeaders().get("Content-Type");
259+
assertEquals("text/html", contentType, "Content-Type should not be modified when user explicitly sets it");
260+
}
261+
262+
@RepeatedIfExceptionsTest(repeats = 5)
263+
public void testUserSetContentTypeWithCharsetShouldBePreserved() {
264+
Request request = post("http://localhost/test")
265+
.setHeader("Content-Type", "text/xml; charset=ISO-8859-1")
266+
.setBody("<test>Hello</test>")
267+
.build();
268+
269+
String contentType = request.getHeaders().get("Content-Type");
270+
assertEquals("text/xml; charset=ISO-8859-1", contentType, "User-specified charset should be preserved");
271+
assertTrue(contentType.contains("ISO-8859-1"), "ISO-8859-1 charset should be preserved");
272+
assertFalse(contentType.contains("UTF-8"), "UTF-8 should not be added");
273+
}
274+
275+
@RepeatedIfExceptionsTest(repeats = 5)
276+
public void testApplicationJsonContentTypeShouldNotBeModified() {
277+
Request request = post("http://localhost/test")
278+
.setHeader("Content-Type", "application/json")
279+
.setBody("{\"key\": \"value\"}")
280+
.build();
281+
282+
String contentType = request.getHeaders().get("Content-Type");
283+
assertEquals("application/json", contentType, "application/json should not be modified");
284+
assertFalse(contentType.contains("charset"), "Charset should not be added to application/json");
285+
}
286+
287+
@RepeatedIfExceptionsTest(repeats = 5)
288+
public void testAddHeaderContentTypeShouldNotBeModified() {
289+
Request request = post("http://localhost/test")
290+
.addHeader("Content-Type", "text/plain")
291+
.setBody("Hello World")
292+
.build();
293+
294+
String contentType = request.getHeaders().get("Content-Type");
295+
assertEquals("text/plain", contentType, "Content-Type set via addHeader should not be modified");
296+
}
297+
298+
@RepeatedIfExceptionsTest(repeats = 5)
299+
public void testSetHeadersWithHttpHeadersShouldLockContentType() {
300+
HttpHeaders httpHeaders = new DefaultHttpHeaders();
301+
httpHeaders.set("Content-Type", "text/plain");
302+
303+
Request request = post("http://localhost/test")
304+
.setHeaders(httpHeaders)
305+
.setBody("Hello World")
306+
.build();
307+
308+
String contentType = request.getHeaders().get("Content-Type");
309+
assertEquals("text/plain", contentType, "Content-Type set via setHeaders(HttpHeaders) should not be modified");
310+
}
311+
312+
@RepeatedIfExceptionsTest(repeats = 5)
313+
public void testSetHeadersWithMapShouldLockContentType() {
314+
Map<String, List<String>> headerMap = new HashMap<>();
315+
headerMap.put("Content-Type", singletonList("text/plain"));
316+
317+
Request request = post("http://localhost/test")
318+
.setHeaders(headerMap)
319+
.setBody("Hello World")
320+
.build();
321+
322+
String contentType = request.getHeaders().get("Content-Type");
323+
assertEquals("text/plain", contentType, "Content-Type set via setHeaders(Map) should not be modified");
324+
}
325+
326+
@RepeatedIfExceptionsTest(repeats = 5)
327+
public void testSetSingleHeadersShouldLockContentType() {
328+
Map<String, String> headerMap = new HashMap<>();
329+
headerMap.put("Content-Type", "text/plain");
330+
331+
Request request = post("http://localhost/test")
332+
.setSingleHeaders(headerMap)
333+
.setBody("Hello World")
334+
.build();
335+
336+
String contentType = request.getHeaders().get("Content-Type");
337+
assertEquals("text/plain", contentType, "Content-Type set via setSingleHeaders should not be modified");
338+
}
339+
340+
@RepeatedIfExceptionsTest(repeats = 5)
341+
public void testClearHeadersShouldResetContentTypeLock() {
342+
Request request = post("http://localhost/test")
343+
.setHeader("Content-Type", "text/plain")
344+
.clearHeaders()
345+
.setHeader("Content-Type", "text/xml")
346+
.setBody("<test></test>")
347+
.build();
348+
349+
String contentType = request.getHeaders().get("Content-Type");
350+
assertEquals("text/xml", contentType, "Content-Type should still be preserved after clear and re-set");
351+
}
352+
353+
@RepeatedIfExceptionsTest(repeats = 5)
354+
public void testPrototypeRequestShouldPreserveContentType() {
355+
Request original = post("http://localhost/test")
356+
.setHeader("Content-Type", "text/plain")
357+
.setBody("Hello")
358+
.build();
359+
360+
Request copy = post("http://localhost/test")
361+
.setUrl(original.getUri().toUrl())
362+
.setHeaders(original.getHeaders())
363+
.setBody("Hello")
364+
.build();
365+
366+
String contentType = copy.getHeaders().get("Content-Type");
367+
assertEquals("text/plain", contentType, "Content-Type should be preserved from prototype");
368+
}
369+
370+
@RepeatedIfExceptionsTest(repeats = 5)
371+
public void testRequestBuilderFromPrototypeShouldPreserveContentType() {
372+
Request original = post("http://localhost/test")
373+
.setHeader("Content-Type", "text/plain")
374+
.setBody("Hello")
375+
.build();
376+
377+
Request copy = new RequestBuilder(original).build();
378+
379+
String contentType = copy.getHeaders().get("Content-Type");
380+
assertEquals("text/plain", contentType, "Content-Type should be preserved from prototype via RequestBuilder");
381+
}
382+
383+
@RepeatedIfExceptionsTest(repeats = 5)
384+
public void testCaseInsensitiveContentTypeHeader() {
385+
Request request = post("http://localhost/test")
386+
.setHeader("content-type", "text/plain")
387+
.setBody("Hello World")
388+
.build();
389+
390+
String contentType = request.getHeaders().get("Content-Type");
391+
assertEquals("text/plain", contentType, "Content-Type should be matched case-insensitively");
392+
}
393+
394+
@RepeatedIfExceptionsTest(repeats = 5)
395+
public void testSetHeaderWithIterableShouldLockContentType() {
396+
Request request = post("http://localhost/test")
397+
.setHeader("Content-Type", singletonList("text/plain"))
398+
.setBody("Hello World")
399+
.build();
400+
401+
String contentType = request.getHeaders().get("Content-Type");
402+
assertEquals("text/plain", contentType, "Content-Type set via setHeader(Iterable) should not be modified");
403+
}
404+
405+
@RepeatedIfExceptionsTest(repeats = 5)
406+
public void testAddHeaderWithIterableShouldLockContentType() {
407+
Request request = post("http://localhost/test")
408+
.addHeader("Content-Type", singletonList("text/plain"))
409+
.setBody("Hello World")
410+
.build();
411+
412+
String contentType = request.getHeaders().get("Content-Type");
413+
assertEquals("text/plain", contentType, "Content-Type set via addHeader(Iterable) should not be modified");
414+
}
223415
}

0 commit comments

Comments
 (0)