I first encountered Stack Overflow question no. 79616625, reporting duplicate Content-Type
headers in REST docs, specifically in request docs.
A reproducer can be found here at https://github.com/Jeffrey-Oh/restassured-restdocs-issue.
The culprit seems to be method MockHttpServletRequest::addHeader
. Its current implementation has a problem:
public void addHeader(String name, Object value) {
if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name) &&
!this.headers.containsKey(HttpHeaders.CONTENT_TYPE)) {
setContentType(value.toString());
}
else if (HttpHeaders.ACCEPT_LANGUAGE.equalsIgnoreCase(name) &&
!this.headers.containsKey(HttpHeaders.ACCEPT_LANGUAGE)) {
try {
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.ACCEPT_LANGUAGE, value.toString());
List<Locale> locales = headers.getAcceptLanguageAsLocales();
this.locales.clear();
this.locales.addAll(locales);
if (this.locales.isEmpty()) {
this.locales.add(Locale.ENGLISH);
}
}
catch (IllegalArgumentException ex) {
// Invalid Accept-Language format -> just store plain header
}
doAddHeaderValue(name, value, true);
}
else {
doAddHeaderValue(name, value, false);
}
}
Please note, how a Content-Type
header is only set if it does not exist yet. If it already exists and is set again, at the end of the method we fall through to doAddHeaderValue(name, value, false)
, adding a second value to the already existing header. Technically, the underlying data structure is a map with the header name being the key and a HeaderValueHolder
being able to hold multiple values. For Content-Type
, however, only a single value is allowed, and there also should not be multiple instances of this header. By the way, the value false
in doAddHeaderValue(name, value, false)
means to not replace but amend the value list.
I have not checked the logic for the similar case Accept-Language
, but AFAIK there multiple values are allowed - not sure about multiple occurrences of the header itself though.
Please, fix the logic for Content-Type
.
CC @Jeffrey-Oh
Comment From: kriegaex
A simple change would be to always set (i.e. overwrite, not add) the Content-Type
header (untested change):
if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) {
setContentType(value.toString());
}
I also forgot to link to some REST docs with duplicate headers:
- https://github.com/Jeffrey-Oh/restassured-restdocs-issue/blob/master/build/generated-snippets/user/logout/http-request.adoc
- https://github.com/Jeffrey-Oh/restassured-restdocs-issue/blob/master/build/generated-snippets/user/logout/httpie-request.adoc
- https://github.com/Jeffrey-Oh/restassured-restdocs-issue/blob/master/build/generated-snippets/user/logout/curl-request.adoc
Comment From: kriegaex
Thanks for the swift reaction, @bclozel. 🙂🙏
Comment From: bclozel
Thanks for the detailed report @kriegaex
Comment From: erizzo-cfa
Sorry to comment on a closed issue, but the change made to "fix" this resulted in what I think is an unintended consequence (regression?): a Content-Type
header will always override a value that was set with setContentType()
. It's not obvious that an explicit call to setContentType()
will be overridden by a Content-Type
header that's been set in the builder. In fact, I find it surprising that the header replaces the explicit value - I doubt I'm the only person who'd be surprised by that.
Doesn't it seem that this part of the behavior change was unintentional and unnecessary to address the original issue (duplicate Content-Type header values)?
Comment From: erizzo-cfa
In fact, it seems to break MockMultipartHttpServletRequestBuilder
which has an explicit call to contentType(MediaType.MULTIPART_FORM_DATA)
in its constructor. Multipart requests that have a content type header other than multipart/form-data
typically don't work with "upload" type endpoints (which is how I discovered this issue, when some of our tests started failing after the update to Spring). If a MockMvc is set up in a typical fashion with some default headers (application/json
is often used for REST endpoint tests), then code that uses MockMvcRequestBuilders.multipart(String, Object...)
is now sending the wrong content-type, which it wasn't doing before.
Comment From: bclozel
Please create a new issue for this and share a code snippet that demonstrates the problem