The exception classes that we create for our application can be annotated with @ResponseStatus. An unhandled exception that is an instance of such a class will carry the specified HTTP status code to the client.
If such an exception is wrapped inside another exception, the status code will still be sent in the response, because the underlying exception resolver looks recursively for @ResponseStatus on cause exceptions (since Spring 4.2).
This feature is implemented by ResponseStatusExceptionResolver (another implementation of HandlerExceptionResolver). By default, an instance of this resolver is auto-registered with the DispatcherServlet, so no additional configuration is required to use it.
Note: Since Spring 5.0, an alternative to defining a dedicated exception class is to throw a ResponseStatusException directly from the handler method, which lets you set the status (and an optional reason) programmatically without creating a new exception type. The approach shown below, using a custom exception class annotated with @ResponseStatus, remains fully supported and is still preferable when the exception type itself carries semantic meaning elsewhere in the application.
Example
Using @ResponseStatus on an exception class
package com.logicbig.example;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.FORBIDDEN)
public class UserNotLoggedInException extends Exception {
public UserNotLoggedInException (String message) {
super(message);
}
}
A controller handler throwing the exception
package com.logicbig.example;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import jakarta.servlet.http.HttpServletRequest;
@Controller
public class ExampleController {
@RequestMapping("/admin")
@ResponseBody
public String handleRequest (HttpServletRequest request)
throws UserNotLoggedInException {
Object user = request.getSession()
.getAttribute("user");
if (user == null) {
throw new UserNotLoggedInException("user: " + user);
}
return "test response " + user;
}
//nested exceptions
@RequestMapping("/test")
public void handleRequest2 () throws Exception {
throw new Exception(new UserNotLoggedInException(null));
}
}
The exception thrown should not be handled elsewhere in code or by other exception resolvers — for example, it shouldn't be handled by @ExceptionHandler, because doing so would override the status code specified on the exception class via @ResponseStatus.
Running the example
To try examples, run embedded Jetty (configured in pom.xml of example project below):
mvn jetty:run
Output
/admin
/test
In this case, we wrapped UserNotLoggedInException inside a java.lang.Exception instance (only works Spring starting MVC 4.2.0.RELEASE)
Without @ResponseStatus on the exception class:
If we remove @ResponseStatus from UserNotLoggedInException.java, then:
Normally, any unhandled exception thrown will return an HTTP 500 response (Internal Server Error).
Integration Test
package com.logicbig.example;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.web.context.WebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
@SpringJUnitWebConfig(AppConfig.class)
public class ExampleControllerTest {
@Autowired
private WebApplicationContext wac;
private MockMvcTester mockMvcTester;
@BeforeEach
public void setup() {
mockMvcTester = MockMvcTester.from(wac);
}
@Test
public void admin_withoutSession_returnsForbidden() {
assertThat(mockMvcTester.get().uri("/admin").exchange())
.hasStatus(HttpStatus.FORBIDDEN);
}
@Test
public void admin_withSession_returnsOkWithBody() {
MockHttpSession session = new MockHttpSession();
session.setAttribute("user", "joe");
assertThat(mockMvcTester.get().uri("/admin").session(session)
.exchange())
.hasStatusOk()
.bodyText().isEqualTo("test response joe");
}
@Test
public void test_wrappedException_returnsForbidden() {
// On Spring 4.2.0, ResponseStatusExceptionResolver looks recursively at
// cause exceptions, so the @ResponseStatus(FORBIDDEN) on the wrapped
// UserNotLoggedInException is honored even though a plain Exception is thrown.
assertThat(mockMvcTester.get().uri("/test").exchange())
.hasStatus(HttpStatus.FORBIDDEN);
}
}
mvn clean test -Dtest="ExampleControllerTest" Output$ mvn clean test -Dtest="ExampleControllerTest" [INFO] Scanning for projects... [INFO] [INFO] --------< com.logicbig.example:exception-type-response-status >--------- [INFO] Building exception-type-response-status 1.0-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ war ]--------------------------------- [INFO] [INFO] --- clean:3.2.0:clean (default-clean) @ exception-type-response-status --- [INFO] Deleting D:\example-projects\spring-mvc\exception-type-response-status\target [INFO] [INFO] --- resources:3.3.1:resources (default-resources) @ exception-type-response-status --- [INFO] skip non existing resourceDirectory D:\example-projects\spring-mvc\exception-type-response-status\src\main\resources [INFO] [INFO] --- compiler:3.16.0:compile (default-compile) @ exception-type-response-status --- [INFO] Recompiling the module because of changed source code. [INFO] Compiling 4 source files with javac [debug target 25] to target\classes [INFO] [INFO] --- resources:3.3.1:testResources (default-testResources) @ exception-type-response-status --- [INFO] skip non existing resourceDirectory D:\example-projects\spring-mvc\exception-type-response-status\src\test\resources [INFO] [INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ exception-type-response-status --- [INFO] Recompiling the module because of changed dependency. [INFO] Compiling 2 source files with javac [debug target 25] to target\test-classes [INFO] [INFO] --- surefire:3.2.5:test (default-test) @ exception-type-response-status --- [INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider [INFO] [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.logicbig.example.ExampleControllerTest [INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.355 s -- in com.logicbig.example.ExampleControllerTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 7.338 s [INFO] Finished at: 2026-09-18T07:44:44-05:00 [INFO] ------------------------------------------------------------------------ INFO: Completed initialization in 2 ms INFO: Completed initialization in 1 ms INFO: Completed initialization in 1 ms
Other uses of @ResponseStatus
- It can be used on the controller's handler methods, i.e. along with @RequestMapping (or a variants such as @GetMapping/@PostMapping). It can also be used at the @Controller class level, in which case it is inherited by all methods.
- It can be used on the exception handler methods, i.e. along with @ExceptionHandler.
Example ProjectDependencies and Technologies Used: - spring-webmvc 7.0.6 (Spring Web MVC)
Version Compatibility: 4.2.0.RELEASE - 7.0.6 Version compatibilities of spring-webmvc with this example: Versions in green have been tested.
- spring-test 7.0.6 (Spring TestContext Framework)
- jakarta.servlet-api 6.1.0 (Jakarta Servlet API documentation)
- junit-jupiter-engine 6.0.3 (Module "junit-jupiter-engine" of JUnit)
- hamcrest 3.0 (Core API and libraries of hamcrest matcher framework)
- assertj-core 3.26.3 (Rich and fluent assertions for testing in Java)
- JDK 25
- Maven 3.9.11
|