Skip to content

Commit 94e8007

Browse files
authored
Merge pull request #50346 from aureamunoz/jakarta-reactive-support
Support HttpServerRequest and HttpServerResponse in Spring ExceptionHandlers
2 parents b9027fa + 1b2a54e commit 94e8007

File tree

4 files changed

+98
-3
lines changed

4 files changed

+98
-3
lines changed

docs/src/main/asciidoc/spring-web.adoc

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,46 @@ The following parameter types are supported, in arbitrary order:
469469

470470
Other parameter types mentioned in the Spring `https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/ExceptionHandler.html[ExceptionHandler javadoc]` are not supported.
471471

472+
==== Using `HttpServerRequest` and `HttpServerResponse` in Spring Web-style exception handlers
473+
474+
When using the Quarkus Spring Web compatibility layer on top of the reactive stack (`quarkus-rest-jackson`),
475+
you cannot inject servlet-based request and response types such as `jakarta.servlet.http.HttpServletRequest` or `jakarta.servlet.http.HttpServletResponse`.
476+
These types are only available when using the Classic RESTEasy stack (quarkus-resteasy / quarkus-resteasy-jackson), which relies on the Servlet API provided by `quarkus-undertow` dependency.
477+
478+
To provide a Spring-friendly experience while staying reactive, Quarkus allows you to inject the following types into your `@ExceptionHandler` methods:
479+
480+
- io.vertx.core.http.HttpServerRequest
481+
482+
- io.vertx.core.http.HttpServerResponse
483+
484+
These types give you direct access to the underlying Vert.x HTTP layer.
485+
This allows you to manipulate the response (e.g. set headers, cookies, or status codes) or enrich the returned information using details from the request.
486+
487+
For example:
488+
489+
[source, java]
490+
----
491+
@ExceptionHandler(IllegalArgumentException.class)
492+
public ResponseEntity<Object> handleException(Exception ex,
493+
HttpServerRequest request,
494+
HttpServerResponse response) {
495+
// Add a custom header to the response
496+
response.putHeader("X-Error-Reason", "IllegalArgument");
497+
498+
// Build a response body enriched with request details
499+
String body = String.format("Request %s %s failed",
500+
request.method().name(),
501+
request.uri());
502+
503+
return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);
504+
}
505+
506+
----
507+
508+
**Warning:** This is potentially dangerous.
509+
Users should only do this if they understand the reactive model, and are aware that writing directly to the response can bypass normal handling of status codes, headers, and serialization.
510+
511+
472512
== Important Technical Note
473513

474514
Please note that the Spring support in Quarkus does not start a Spring Application Context nor are any Spring infrastructure classes run.

extensions/spring-web/core/deployment/src/main/java/io/quarkus/spring/web/deployment/ControllerAdviceExceptionMapperGenerator.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
import io.quarkus.gizmo.MethodDescriptor;
3333
import io.quarkus.gizmo.ResultHandle;
3434
import io.quarkus.spring.web.runtime.common.ResponseEntityConverter;
35+
import io.vertx.core.http.HttpServerRequest;
36+
import io.vertx.core.http.HttpServerResponse;
3537

3638
class ControllerAdviceExceptionMapperGenerator extends AbstractExceptionMapperGenerator {
3739

@@ -250,6 +252,16 @@ private ResultHandle invokeExceptionHandlerMethod(MethodCreator toResponse) {
250252
parameterTypeHandles[i] = getBeanFromArc(toResponse, UriInfo.class.getName());
251253
} else if (typesUtil.isAssignable(Request.class, parameterType.name())) {
252254
parameterTypeHandles[i] = getBeanFromArc(toResponse, Request.class.getName());
255+
} else if (typesUtil.isAssignable(HttpServerRequest.class, parameterType.name())) {
256+
parameterTypeHandles[i] = getBeanFromArc(toResponse, HttpServerRequest.class.getName());
257+
} else if (typesUtil.isAssignable(HttpServerResponse.class, parameterType.name())) {
258+
ResultHandle requestHandle = getBeanFromArc(toResponse, HttpServerRequest.class.getName());
259+
ResultHandle responseHandle = toResponse.invokeInterfaceMethod(
260+
MethodDescriptor.ofMethod(HttpServerRequest.class,
261+
"response",
262+
HttpServerResponse.class),
263+
requestHandle);
264+
parameterTypeHandles[i] = responseHandle;
253265
} else {
254266
throw new IllegalArgumentException(
255267
"Parameter type '" + parameterType.name() + "' is not supported for method '"

extensions/spring-web/resteasy-classic/tests/src/test/java/io/quarkus/spring/web/resteasy/classic/test/ResponseStatusAndExceptionHandlerTest.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static io.restassured.RestAssured.when;
44

55
import jakarta.servlet.http.HttpServletRequest;
6+
import jakarta.servlet.http.HttpServletResponse;
67

78
import org.junit.jupiter.api.Test;
89
import org.junit.jupiter.api.extension.RegisterExtension;
@@ -29,6 +30,11 @@ public void testRootResource() {
2930
when().get("/exception").then().statusCode(400);
3031
}
3132

33+
@Test
34+
public void testIllegalResource() {
35+
when().get("/exception/illegalArgument").then().statusCode(500);
36+
}
37+
3238
@RestController
3339
@RequestMapping("/exception")
3440
public static class ExceptionController {
@@ -40,6 +46,14 @@ public String throwException() {
4046
exception.setStackTrace(new StackTraceElement[0]);
4147
throw exception;
4248
}
49+
50+
@GetMapping("/illegalArgument")
51+
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
52+
public String throwIllegalException() {
53+
IllegalArgumentException exception = new IllegalArgumentException();
54+
exception.setStackTrace(new StackTraceElement[0]);
55+
throw exception;
56+
}
4357
}
4458

4559
@RestControllerAdvice
@@ -50,9 +64,11 @@ public ResponseEntity<Object> handleException(Exception ex) {
5064
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
5165
}
5266

53-
@ExceptionHandler(RuntimeException.class)
54-
public ResponseEntity<Object> forbidden(Exception ex, HttpServletRequest request) {
55-
return new ResponseEntity<>(HttpStatus.FORBIDDEN);
67+
@ExceptionHandler(IllegalArgumentException.class)
68+
public ResponseEntity<Object> handleException(Exception ex, HttpServletRequest request, HttpServletResponse response) {
69+
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
70+
request.setAttribute("javax.servlet.error.status_code", HttpStatus.INTERNAL_SERVER_ERROR.value());
71+
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
5672
}
5773
}
5874
}

extensions/spring-web/resteasy-reactive/tests/src/test/java/io/quarkus/spring/web/resteasy/reactive/test/ResponseStatusAndExceptionHandlerTest.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package io.quarkus.spring.web.resteasy.reactive.test;
22

33
import static io.restassured.RestAssured.when;
4+
import static org.hamcrest.Matchers.containsString;
5+
import static org.hamcrest.Matchers.is;
46

57
import org.junit.jupiter.api.Test;
68
import org.junit.jupiter.api.extension.RegisterExtension;
@@ -13,6 +15,8 @@
1315
import org.springframework.web.bind.annotation.RestControllerAdvice;
1416

1517
import io.quarkus.test.QuarkusUnitTest;
18+
import io.vertx.core.http.HttpServerRequest;
19+
import io.vertx.core.http.HttpServerResponse;
1620

1721
public class ResponseStatusAndExceptionHandlerTest {
1822

@@ -31,6 +35,12 @@ public void testResponseStatusOnException() {
3135
when().get("/exception2").then().statusCode(202);
3236
}
3337

38+
@Test
39+
public void testExceptionHandlingWithHttpRequest() {
40+
when().get("/exception3").then().statusCode(400)
41+
.body(containsString("Request GET /exception3 failed")).header("X-Error-Reason", is("IllegalArgument"));
42+
}
43+
3444
@RestController
3545
public static class ExceptionController {
3646

@@ -50,6 +60,13 @@ public String throwMyException() {
5060
myException.setStackTrace(EMPTY_STACK_TRACE);
5161
throw myException;
5262
}
63+
64+
@GetMapping("/exception3")
65+
public String throwIllegalArgumentException() {
66+
IllegalArgumentException illegalArgumentException = new IllegalArgumentException();
67+
illegalArgumentException.setStackTrace(EMPTY_STACK_TRACE);
68+
throw illegalArgumentException;
69+
}
5370
}
5471

5572
@RestControllerAdvice
@@ -59,6 +76,16 @@ public static class RestExceptionHandler {
5976
public ResponseEntity<Object> handleException(Exception ex) {
6077
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
6178
}
79+
80+
@ExceptionHandler(IllegalArgumentException.class)
81+
public ResponseEntity<Object> handleBadRequestException(Exception ex, HttpServerRequest request,
82+
HttpServerResponse response) {
83+
String body = String.format(
84+
"Request %s %s failed",
85+
request.method().name(), request.uri());
86+
response.putHeader("X-Error-Reason", "IllegalArgument");
87+
return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);
88+
}
6289
}
6390

6491
@ResponseStatus(HttpStatus.ACCEPTED)

0 commit comments

Comments
 (0)