Skip to content

Commit 4932e86

Browse files
authored
Maintain Content-Type set explicitly by client (#2130)
Closes #2129
1 parent a177bd3 commit 4932e86

File tree

2 files changed

+250
-3
lines changed

2 files changed

+250
-3
lines changed

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

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,30 @@ 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+
protected boolean contentTypeLocked;
97+
98+
/**
99+
* Mark the Content-Type header as explicitly set by the user. When locked, the
100+
* Content-Type header will not be modified by the client (e.g., charset addition).
101+
*/
102+
protected final void doContentTypeLock() {
103+
this.contentTypeLocked = true;
104+
}
105+
106+
/**
107+
* Clear the Content-Type lock, allowing the client to modify the Content-Type header
108+
* if needed (for example, to add charset when it was auto-generated).
109+
*/
110+
protected final void resetContentTypeLock() {
111+
this.contentTypeLocked = false;
112+
}
113+
114+
/**
115+
* Return whether the Content-Type header has been locked as explicitly set by the user.
116+
*/
117+
protected final boolean isContentTypeLocked() {
118+
return this.contentTypeLocked;
119+
}
96120

97121
protected RequestBuilderBase(String method, boolean disableUrlEncoding) {
98122
this(method, disableUrlEncoding, true);
@@ -116,6 +140,10 @@ protected RequestBuilderBase(Request prototype, boolean disableUrlEncoding, bool
116140
localAddress = prototype.getLocalAddress();
117141
headers = new DefaultHttpHeaders(validateHeaders);
118142
headers.add(prototype.getHeaders());
143+
// If prototype has Content-Type, consider it as explicitly set
144+
if (headers.contains(CONTENT_TYPE)) {
145+
doContentTypeLock();
146+
}
119147
if (isNonEmpty(prototype.getCookies())) {
120148
cookies = new ArrayList<>(prototype.getCookies());
121149
}
@@ -181,6 +209,7 @@ public T setVirtualHost(String virtualHost) {
181209
*/
182210
public T clearHeaders() {
183211
headers.clear();
212+
resetContentTypeLock();
184213
return asDerivedType();
185214
}
186215

@@ -203,6 +232,9 @@ public T setHeader(CharSequence name, String value) {
203232
*/
204233
public T setHeader(CharSequence name, Object value) {
205234
headers.set(name, value);
235+
if (CONTENT_TYPE.contentEqualsIgnoreCase(name)) {
236+
doContentTypeLock();
237+
}
206238
return asDerivedType();
207239
}
208240

@@ -215,6 +247,9 @@ public T setHeader(CharSequence name, Object value) {
215247
*/
216248
public T setHeader(CharSequence name, Iterable<?> values) {
217249
headers.set(name, values);
250+
if (CONTENT_TYPE.contentEqualsIgnoreCase(name)) {
251+
doContentTypeLock();
252+
}
218253
return asDerivedType();
219254
}
220255

@@ -243,6 +278,9 @@ public T addHeader(CharSequence name, Object value) {
243278
}
244279

245280
headers.add(name, value);
281+
if (CONTENT_TYPE.contentEqualsIgnoreCase(name)) {
282+
doContentTypeLock();
283+
}
246284
return asDerivedType();
247285
}
248286

@@ -256,6 +294,9 @@ public T addHeader(CharSequence name, Object value) {
256294
*/
257295
public T addHeader(CharSequence name, Iterable<?> values) {
258296
headers.add(name, values);
297+
if (CONTENT_TYPE.contentEqualsIgnoreCase(name)) {
298+
doContentTypeLock();
299+
}
259300
return asDerivedType();
260301
}
261302

@@ -264,6 +305,9 @@ public T setHeaders(HttpHeaders headers) {
264305
this.headers.clear();
265306
} else {
266307
this.headers = headers;
308+
if (headers.contains(CONTENT_TYPE)) {
309+
doContentTypeLock();
310+
}
267311
}
268312
return asDerivedType();
269313
}
@@ -278,7 +322,12 @@ public T setHeaders(HttpHeaders headers) {
278322
public T setHeaders(Map<? extends CharSequence, ? extends Iterable<?>> headers) {
279323
clearHeaders();
280324
if (headers != null) {
281-
headers.forEach((name, values) -> this.headers.add(name, values));
325+
headers.forEach((name, values) -> {
326+
this.headers.add(name, values);
327+
if (CONTENT_TYPE.contentEqualsIgnoreCase(name)) {
328+
doContentTypeLock();
329+
}
330+
});
282331
}
283332
return asDerivedType();
284333
}
@@ -293,7 +342,12 @@ public T setHeaders(Map<? extends CharSequence, ? extends Iterable<?>> headers)
293342
public T setSingleHeaders(Map<? extends CharSequence, ?> headers) {
294343
clearHeaders();
295344
if (headers != null) {
296-
headers.forEach((name, value) -> this.headers.add(name, value));
345+
headers.forEach((name, value) -> {
346+
this.headers.add(name, value);
347+
if (CONTENT_TYPE.contentEqualsIgnoreCase(name)) {
348+
doContentTypeLock();
349+
}
350+
});
297351
}
298352
return asDerivedType();
299353
}
@@ -634,7 +688,8 @@ private void updateCharset() {
634688
String contentTypeHeader = headers.get(CONTENT_TYPE);
635689
Charset contentTypeCharset = extractContentTypeCharsetAttribute(contentTypeHeader);
636690
charset = withDefault(contentTypeCharset, withDefault(charset, UTF_8));
637-
if (contentTypeHeader != null && contentTypeHeader.regionMatches(true, 0, "text/", 0, 5) && contentTypeCharset == null) {
691+
// Only add charset if Content-Type was not explicitly set by user
692+
if (!isContentTypeLocked() && contentTypeHeader != null && contentTypeHeader.regionMatches(true, 0, "text/", 0, 5) && contentTypeCharset == null) {
638693
// add explicit charset to content-type header
639694
headers.set(CONTENT_TYPE, contentTypeHeader + "; charset=" + charset.name());
640695
}

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)