I'm currently writing tests using RestAssuredMockMvc
together with Spring REST Docs.
In my global spec setup, I configure the headers like this:
package com.animore.config
import com.fasterxml.jackson.databind.ObjectMapper
import io.restassured.http.ContentType
import io.restassured.module.mockmvc.RestAssuredMockMvc
import io.restassured.module.mockmvc.specification.MockMvcRequestSpecification
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs
import org.springframework.context.annotation.Import
import org.springframework.http.HttpHeaders
import org.springframework.restdocs.RestDocumentationContextProvider
import org.springframework.restdocs.RestDocumentationExtension
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.web.context.WebApplicationContext
import spock.lang.Specification
import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyUris
import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint
@Import([TestWebConfig, TestSecurityConfig])
@AutoConfigureRestDocs
@ExtendWith([RestDocumentationExtension.class])
abstract class ControllerTest extends Specification {
@Autowired
WebApplicationContext context
@Autowired
RestDocumentationContextProvider restDocumentation
protected MockMvcRequestSpecification spec
@Autowired
ObjectMapper objectMapper
def setup() {
spec = RestAssuredMockMvc
.given()
.headers(
HttpHeaders.AUTHORIZATION, "Bearer accessToken",
HttpHeaders.ACCEPT, ContentType.JSON,
HttpHeaders.CONTENT_TYPE, ContentType.JSON
)
.mockMvc(
MockMvcBuilders.webAppContextSetup(context)
.apply(
MockMvcRestDocumentation.documentationConfiguration(restDocumentation)
.operationPreprocessors()
.withRequestDefaults(
modifyUris()
.scheme("https")
.host("docs.api.com")
.removePort(),
prettyPrint()
)
.withResponseDefaults(prettyPrint())
)
.build())
.log().all()
}
}
Then, in an actual test case, I call:
package com.animore.user.presentation
import com.animore.common.web.message.SuccessMessage
import com.animore.config.ControllerTest
import com.animore.config.JwtTokenProvider
import com.animore.user.application.usecase.UserUseCase
import io.restassured.module.mockmvc.RestAssuredMockMvc
import org.spockframework.spring.SpringBean
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation
import static org.hamcrest.Matchers.equalTo
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields
@WebMvcTest(UserController.class)
class UserControllerSpec extends ControllerTest {
@SpringBean
private JwtTokenProvider jwtTokenProvider = Mock()
@SpringBean
private UserUseCase userUseCase = Mock()
def "사용자 로그아웃 restassured version"() {
expect:
RestAssuredMockMvc.given()
.spec(spec)
.when()
.patch("/api/user/logout")
.then()
.statusCode(200)
.body("code", equalTo(200))
.body("message", equalTo(SuccessMessage.SUCCESS_MSG))
.body("data", equalTo(null))
.apply(
MockMvcRestDocumentation.document(
"user/logout",
// preprocessRequest(modifyHeaders().set("Content-Type", "application/json")),
responseFields(
fieldWithPath("code").description("응답 코드"),
fieldWithPath("message").description("응답 메시지"),
fieldWithPath("data").description("응답 데이터")
)
)
)
}
}
In the generated http-request.adoc
, I see two Content-Type
headers:
Content-Type: application/json
Content-Type: application/json
Interestingly, if I uncomment the modifyHeaders().set(...)
line, the duplicate goes away and only one remains.
I debugged this and found that inside Spring’s MockHttpServletRequest
, the Content-Type
header is already set twice before the documentation handler even touches it. So the problem occurs earlier — either in RestAssured or Spring MockMvc setup.
Spring’s MockHttpServletRequest#addHeader(...)
method has logic like:
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);
}
}
Which means:
Even if Content-Type
is already set, a duplicate may be added later unless filtered manually.
I understand this might be intentional — addHeader()
is designed to allow multiple headers and be neutral — not prevent duplication. Still, when combined with RestAssured and Spring REST Docs, it creates an awkward result in http-request.adoc
.
// Spring REST Docs
testImplementation("org.springframework.restdocs:spring-restdocs-restassured:3.0.3")
testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc:3.0.3")
// RestAssured + Spring MockMvc
testImplementation("io.rest-assured:spring-mock-mvc:5.5.1") {
exclude(group = "org.springframework", module = "spring-webmvc")
}
testImplementation("org.springframework:spring-webmvc:6.2.6")
testImplementation("io.rest-assured:rest-assured:5.5.1") {
exclude(group = "org.apache.johnzon", module = "johnzon-mapper")
exclude(group = "org.codehaus.jackson", module = "jackson-mapper-asl")
exclude(group = "commons-io", module = "commons-io")
}
testImplementation("commons-io:commons-io:2.19.0")
// Spock
testImplementation("org.spockframework:spock-core:2.4-M6-groovy-4.0")
testImplementation("org.spockframework:spock-spring:2.4-M6-groovy-4.0")
RestAssuredMockMvc
with Spring REST Docs?Content-Type
from being added twice?modifyHeaders().set(...)
), by RestAssured, or by Spring?Thanks in advance for any insights. 🙏
I debugged through the Spring code and also ended up finding method MockHttpServletRequest::addHeader
suspicious. IMO, it contains a bug in case of Content-Type
headers. Therefore, I created Spring issue #34913.
Update: The bug has been fixed and will probably be released with Spring 6.2.8.