Hello Spring team 👋 ,
I continue my work on Spring MVC + functional endpoints (as part of a migration from webflux to standard), I think I've found another limitation compared to the WebFlux version.
I would like to test a very simple application:
@SpringBootApplication
class TesterrorsApplication
fun main(args: Array<String>) {
runApplication<TesterrorsApplication>(*args)
}
@Configuration
@Import(Handler::class)
class Router {
@Bean
fun routes(h: Handler) = router {
GET("gone", h::notFound)
}
}
class Handler {
fun notFound(r: ServerRequest): ServerResponse = throw ResponseStatusException(HttpStatus.GONE, "sorry…")
}
However, I found no way to get the "body" out of the error during test. With the following code, the value v is empty.
@WebMvcTest(controllers = [Handler::class])
@Import(Router::class)
@ImportAutoConfiguration(ErrorMvcAutoConfiguration::class)
class HandlerTest(
@Autowired val rest: WebTestClient
) {
@Test
fun `should return gone with details`() {
/* Given */
/* When */
val v = rest
.get()
.uri("/gone")
/* Then */
.exchange()
.expectStatus()
.isEqualTo(HttpStatus.GONE)
.expectBody()
.returnResult()
.responseBody
?.let(::String)!!
/* Then */
assertThat(v).isNotEmpty()
/*
Where something like this is expected:
{
"timestamp": "2024-04-28T14:43:57.477+00:00",
"status": 404,
"error": "Not Found",
"path": "/notfound"
}
*/
}
}
The strange part is the BasicErrorController is instantiated during test phase, but I don't know why error(HttpServletRequest request) is not called.
Of course, this works perfectly fine in "prod", when the app is run locally:
It's important to note I use the same pattern (with @ImportAutoConfiguration(ErrorWebFluxAutoConfiguration::class)) for many years successfully, so I was surprised to hit such a case with MVC + functionnal endpoints.
The code is available in this repository
Thank you for your help and support 👋
Comment From: wilkinsona
This is due to a difference in how errors are handled by WebFlux vs how they're handled by the servlet spec. The latter has the concept of error pages, with which Spring Boot integrates, that rely on the request being forwarded to the correct error page. MockMvc does not have complete support for such forwarding so some additional manual steps are required. You can learn more and see some suggestions in https://github.com/spring-projects/spring-boot/issues/5574.
Comment From: davinkevin
For others reading this thread, I used this code:
class MockMvcRestExceptionConfiguration(
private val errorController: BasicErrorController,
private val om: ObjectMapper
) : WebMvcConfigurer {
override fun addInterceptors(registry: InterceptorRegistry) {
registry.addInterceptor(object : HandlerInterceptor {
@Throws(Exception::class)
override fun afterCompletion(
request: HttpServletRequest,
response: HttpServletResponse,
handler: Any,
@Nullable ex: java.lang.Exception?
) {
val status: Int = response.status
if (status < 400) return
request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, status)
request.setAttribute(WebUtils.ERROR_STATUS_CODE_ATTRIBUTE, status)
request.setAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE, request.requestURI.toString())
// The original exception is already saved as an attribute request
when (val exception = request.getAttribute(DispatcherServlet.EXCEPTION_ATTRIBUTE) as Exception?) {
null -> {}
is ResponseStatusException -> request.apply {
setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, exception)
setAttribute(WebUtils.ERROR_MESSAGE_ATTRIBUTE, exception.reason)
}
}
om.writeValue(response.outputStream, errorController.error(request).body)
}
})
}
}
@WebMvcTest(controllers = […])
@Import(…, MockMvcRestExceptionConfiguration::class)
class ItemHandlerTest(…) { … }