Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

No error body when using "functional Handlers" and Spring MVC during tests #40558

Closed
davinkevin opened this issue Apr 28, 2024 · 2 comments
Closed
Labels
status: duplicate A duplicate of another issue

Comments

@davinkevin
Copy link
Contributor

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:

image

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 👋

@davinkevin davinkevin changed the title No error body when using functional Handlers and Spring MVC during tests No error body when using "functional Handlers" and Spring MVC during tests Apr 28, 2024
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Apr 28, 2024
@snicoll snicoll transferred this issue from spring-projects/spring-framework Apr 29, 2024
@wilkinsona
Copy link
Member

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 #5574.

@wilkinsona wilkinsona closed this as not planned Won't fix, can't repro, duplicate, stale Apr 29, 2024
@wilkinsona wilkinsona added status: duplicate A duplicate of another issue and removed status: waiting-for-triage An issue we've not yet triaged labels Apr 29, 2024
@davinkevin
Copy link
Contributor Author

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(…) { … }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: duplicate A duplicate of another issue
Projects
None yet
Development

No branches or pull requests

3 participants