Skip to content

Commit 08a951a

Browse files
authored
Merge pull request eugenp#6144 from rozagerardo/geroza/BAEL-11599_Update-exception-handling-rest-article
[BAEL-11599] Update and move code for the "Error Handling for REST with Spring" article
2 parents 3c7480d + dea88da commit 08a951a

17 files changed

+270
-110
lines changed

spring-boot-rest/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
Module for the articles that are part of the Spring REST E-book:
22

33
1. [Bootstrap a Web Application with Spring 5](https://www.baeldung.com/bootstraping-a-web-application-with-spring-and-java-based-configuration)
4+
2. [Error Handling for REST with Spring](http://www.baeldung.com/exception-handling-for-rest-with-spring)

spring-boot-rest/pom.xml

+19
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,30 @@
2020
<groupId>org.springframework.boot</groupId>
2121
<artifactId>spring-boot-starter-web</artifactId>
2222
</dependency>
23+
<dependency>
24+
<groupId>com.fasterxml.jackson.dataformat</groupId>
25+
<artifactId>jackson-dataformat-xml</artifactId>
26+
</dependency>
27+
<dependency>
28+
<groupId>org.hibernate</groupId>
29+
<artifactId>hibernate-entitymanager</artifactId>
30+
</dependency>
31+
<dependency>
32+
<groupId>org.springframework</groupId>
33+
<artifactId>spring-jdbc</artifactId>
34+
</dependency>
2335

2436
<dependency>
2537
<groupId>org.springframework.boot</groupId>
2638
<artifactId>spring-boot-starter-test</artifactId>
2739
<scope>test</scope>
2840
</dependency>
41+
<dependency>
42+
<groupId>net.sourceforge.htmlunit</groupId>
43+
<artifactId>htmlunit</artifactId>
44+
<version>${htmlunit.version}</version>
45+
<scope>test</scope>
46+
</dependency>
2947
</dependencies>
3048

3149
<build>
@@ -39,5 +57,6 @@
3957

4058
<properties>
4159
<start-class>com.baeldung.SpringBootRestApplication</start-class>
60+
<htmlunit.version>2.32</htmlunit.version>
4261
</properties>
4362
</project>

spring-boot-rest/src/main/java/com/baeldung/web/SpringBootRestApplication.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.baeldung;
1+
package com.baeldung.web;
22

33
import org.springframework.boot.SpringApplication;
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.baeldung.web.config;
2+
3+
import java.util.Map;
4+
5+
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
6+
import org.springframework.stereotype.Component;
7+
import org.springframework.web.context.request.WebRequest;
8+
9+
@Component
10+
public class MyCustomErrorAttributes extends DefaultErrorAttributes {
11+
12+
@Override
13+
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
14+
Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace);
15+
errorAttributes.put("locale", webRequest.getLocale()
16+
.toString());
17+
errorAttributes.remove("error");
18+
errorAttributes.put("cause", errorAttributes.get("message"));
19+
errorAttributes.remove("message");
20+
errorAttributes.put("status", String.valueOf(errorAttributes.get("status")));
21+
return errorAttributes;
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.baeldung.web.config;
2+
3+
import java.util.Map;
4+
5+
import javax.servlet.http.HttpServletRequest;
6+
7+
import org.springframework.boot.autoconfigure.web.ErrorProperties;
8+
import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController;
9+
import org.springframework.boot.web.servlet.error.ErrorAttributes;
10+
import org.springframework.http.HttpStatus;
11+
import org.springframework.http.MediaType;
12+
import org.springframework.http.ResponseEntity;
13+
import org.springframework.stereotype.Component;
14+
import org.springframework.web.bind.annotation.RequestMapping;
15+
16+
@Component
17+
public class MyErrorController extends BasicErrorController {
18+
19+
public MyErrorController(ErrorAttributes errorAttributes) {
20+
super(errorAttributes, new ErrorProperties());
21+
}
22+
23+
@RequestMapping(produces = MediaType.APPLICATION_XML_VALUE)
24+
public ResponseEntity<Map<String, Object>> xmlError(HttpServletRequest request) {
25+
Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.APPLICATION_XML));
26+
body.put("xmlkey", "the XML response is different!");
27+
HttpStatus status = getStatus(request);
28+
return new ResponseEntity<>(body, status);
29+
}
30+
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.baeldung.web.controller;
2+
3+
import org.springframework.http.ResponseEntity;
4+
import org.springframework.web.bind.annotation.GetMapping;
5+
import org.springframework.web.bind.annotation.RestController;
6+
7+
@RestController
8+
public class FaultyRestController {
9+
10+
@GetMapping("/exception")
11+
public ResponseEntity<Void> requestWithException() {
12+
throw new RuntimeException("Error in the faulty controller!");
13+
}
14+
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package com.baeldung.web.error;
2+
3+
import javax.persistence.EntityNotFoundException;
4+
5+
import org.hibernate.exception.ConstraintViolationException;
6+
import org.springframework.dao.DataAccessException;
7+
import org.springframework.dao.DataIntegrityViolationException;
8+
import org.springframework.dao.InvalidDataAccessApiUsageException;
9+
import org.springframework.http.HttpHeaders;
10+
import org.springframework.http.HttpStatus;
11+
import org.springframework.http.ResponseEntity;
12+
import org.springframework.http.converter.HttpMessageNotReadableException;
13+
import org.springframework.web.bind.MethodArgumentNotValidException;
14+
import org.springframework.web.bind.annotation.ControllerAdvice;
15+
import org.springframework.web.bind.annotation.ExceptionHandler;
16+
import org.springframework.web.context.request.WebRequest;
17+
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
18+
19+
import com.baeldung.web.exception.MyResourceNotFoundException;
20+
21+
@ControllerAdvice
22+
public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
23+
24+
public RestResponseEntityExceptionHandler() {
25+
super();
26+
}
27+
28+
// API
29+
30+
// 400
31+
32+
@ExceptionHandler({ ConstraintViolationException.class })
33+
public ResponseEntity<Object> handleBadRequest(final ConstraintViolationException ex, final WebRequest request) {
34+
final String bodyOfResponse = "This should be application specific";
35+
return handleExceptionInternal(ex, bodyOfResponse, new HttpHeaders(), HttpStatus.BAD_REQUEST, request);
36+
}
37+
38+
@ExceptionHandler({ DataIntegrityViolationException.class })
39+
public ResponseEntity<Object> handleBadRequest(final DataIntegrityViolationException ex, final WebRequest request) {
40+
final String bodyOfResponse = "This should be application specific";
41+
return handleExceptionInternal(ex, bodyOfResponse, new HttpHeaders(), HttpStatus.BAD_REQUEST, request);
42+
}
43+
44+
@Override
45+
protected ResponseEntity<Object> handleHttpMessageNotReadable(final HttpMessageNotReadableException ex, final HttpHeaders headers, final HttpStatus status, final WebRequest request) {
46+
final String bodyOfResponse = "This should be application specific";
47+
// ex.getCause() instanceof JsonMappingException, JsonParseException // for additional information later on
48+
return handleExceptionInternal(ex, bodyOfResponse, headers, HttpStatus.BAD_REQUEST, request);
49+
}
50+
51+
@Override
52+
protected ResponseEntity<Object> handleMethodArgumentNotValid(final MethodArgumentNotValidException ex, final HttpHeaders headers, final HttpStatus status, final WebRequest request) {
53+
final String bodyOfResponse = "This should be application specific";
54+
return handleExceptionInternal(ex, bodyOfResponse, headers, HttpStatus.BAD_REQUEST, request);
55+
}
56+
57+
58+
// 404
59+
60+
@ExceptionHandler(value = { EntityNotFoundException.class, MyResourceNotFoundException.class })
61+
protected ResponseEntity<Object> handleNotFound(final RuntimeException ex, final WebRequest request) {
62+
final String bodyOfResponse = "This should be application specific";
63+
return handleExceptionInternal(ex, bodyOfResponse, new HttpHeaders(), HttpStatus.NOT_FOUND, request);
64+
}
65+
66+
// 409
67+
68+
@ExceptionHandler({ InvalidDataAccessApiUsageException.class, DataAccessException.class })
69+
protected ResponseEntity<Object> handleConflict(final RuntimeException ex, final WebRequest request) {
70+
final String bodyOfResponse = "This should be application specific";
71+
return handleExceptionInternal(ex, bodyOfResponse, new HttpHeaders(), HttpStatus.CONFLICT, request);
72+
}
73+
74+
// 412
75+
76+
// 500
77+
78+
@ExceptionHandler({ NullPointerException.class, IllegalArgumentException.class, IllegalStateException.class })
79+
/*500*/public ResponseEntity<Object> handleInternal(final RuntimeException ex, final WebRequest request) {
80+
logger.error("500 Status Code", ex);
81+
final String bodyOfResponse = "This should be application specific";
82+
return handleExceptionInternal(ex, bodyOfResponse, new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR, request);
83+
}
84+
85+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.baeldung.web.exception;
2+
3+
public final class MyResourceNotFoundException extends RuntimeException {
4+
5+
public MyResourceNotFoundException() {
6+
super();
7+
}
8+
9+
public MyResourceNotFoundException(final String message, final Throwable cause) {
10+
super(message, cause);
11+
}
12+
13+
public MyResourceNotFoundException(final String message) {
14+
super(message);
15+
}
16+
17+
public MyResourceNotFoundException(final Throwable cause) {
18+
super(cause);
19+
}
20+
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
### Spring Boot default error handling configurations
2+
#server.error.whitelabel.enabled=false
3+
#server.error.include-stacktrace=always

spring-boot-rest/src/test/java/com/baeldung/web/SpringContextIntegrationTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.baeldung.spring.boot.rest;
1+
package com.baeldung.web;
22

33
import org.junit.Test;
44
import org.junit.runner.RunWith;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.baeldung.web.error;
2+
3+
import static io.restassured.RestAssured.given;
4+
import static org.assertj.core.api.Assertions.assertThat;
5+
import static org.hamcrest.Matchers.hasKey;
6+
import static org.hamcrest.Matchers.is;
7+
import static org.hamcrest.Matchers.isA;
8+
import static org.hamcrest.Matchers.not;
9+
10+
import org.junit.jupiter.api.Test;
11+
import org.springframework.http.HttpHeaders;
12+
import org.springframework.http.MediaType;
13+
14+
import com.gargoylesoftware.htmlunit.WebClient;
15+
import com.gargoylesoftware.htmlunit.html.HtmlPage;
16+
17+
public class ErrorHandlingLiveTest {
18+
19+
private static final String BASE_URL = "http://localhost:8080";
20+
private static final String EXCEPTION_ENDPOINT = "/exception";
21+
22+
private static final String ERROR_RESPONSE_KEY_PATH = "error";
23+
private static final String XML_RESPONSE_KEY_PATH = "xmlkey";
24+
private static final String LOCALE_RESPONSE_KEY_PATH = "locale";
25+
private static final String CAUSE_RESPONSE_KEY_PATH = "cause";
26+
private static final String RESPONSE_XML_ROOT = "Map";
27+
private static final String XML_RESPONSE_KEY_XML_PATH = RESPONSE_XML_ROOT + "." + XML_RESPONSE_KEY_PATH;
28+
private static final String LOCALE_RESPONSE_KEY_XML_PATH = RESPONSE_XML_ROOT + "." + LOCALE_RESPONSE_KEY_PATH;
29+
private static final String CAUSE_RESPONSE_KEY_XML_PATH = RESPONSE_XML_ROOT + "." + CAUSE_RESPONSE_KEY_PATH;
30+
private static final String CAUSE_RESPONSE_VALUE = "Error in the faulty controller!";
31+
private static final String XML_RESPONSE_VALUE = "the XML response is different!";
32+
33+
@Test
34+
public void whenRequestingFaultyEndpointAsJson_thenReceiveDefaultResponseWithConfiguredAttrs() {
35+
given().header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
36+
.get(EXCEPTION_ENDPOINT)
37+
.then()
38+
.body("$", hasKey(LOCALE_RESPONSE_KEY_PATH))
39+
.body(CAUSE_RESPONSE_KEY_PATH, is(CAUSE_RESPONSE_VALUE))
40+
.body("$", not(hasKey(ERROR_RESPONSE_KEY_PATH)))
41+
.body("$", not(hasKey(XML_RESPONSE_KEY_PATH)));
42+
}
43+
44+
@Test
45+
public void whenRequestingFaultyEndpointAsXml_thenReceiveXmlResponseWithConfiguredAttrs() {
46+
given().header(HttpHeaders.ACCEPT, MediaType.APPLICATION_XML_VALUE)
47+
.get(EXCEPTION_ENDPOINT)
48+
.then()
49+
.body(LOCALE_RESPONSE_KEY_XML_PATH, isA(String.class))
50+
.body(CAUSE_RESPONSE_KEY_XML_PATH, is(CAUSE_RESPONSE_VALUE))
51+
.body(RESPONSE_XML_ROOT, not(hasKey(ERROR_RESPONSE_KEY_PATH)))
52+
.body(XML_RESPONSE_KEY_XML_PATH, is(XML_RESPONSE_VALUE));
53+
}
54+
55+
@Test
56+
public void whenRequestingFaultyEndpointAsHtml_thenReceiveWhitelabelPageResponse() throws Exception {
57+
try (WebClient webClient = new WebClient()) {
58+
webClient.getOptions()
59+
.setThrowExceptionOnFailingStatusCode(false);
60+
HtmlPage page = webClient.getPage(BASE_URL + EXCEPTION_ENDPOINT);
61+
assertThat(page.getBody()
62+
.asText()).contains("Whitelabel Error Page");
63+
}
64+
}
65+
}

spring-rest-full/.attach_pid28499

Whitespace-only changes.

spring-rest-full/README.md

-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ The "Learn Spring Security" Classes: http://github.learnspringsecurity.com
1818
- [Metrics for your Spring REST API](http://www.baeldung.com/spring-rest-api-metrics)
1919
- [Bootstrap a Web Application with Spring 4](http://www.baeldung.com/bootstraping-a-web-application-with-spring-and-java-based-configuration)
2020
- [Build a REST API with Spring and Java Config](http://www.baeldung.com/building-a-restful-web-service-with-spring-and-java-based-configuration)
21-
- [Error Handling for REST with Spring](http://www.baeldung.com/exception-handling-for-rest-with-spring)
2221
- [Spring Security Expressions - hasRole Example](https://www.baeldung.com/spring-security-expressions-basic)
2322

2423

spring-rest-full/pom.xml

-43
Original file line numberDiff line numberDiff line change
@@ -212,23 +212,6 @@
212212
<groupId>org.apache.maven.plugins</groupId>
213213
<artifactId>maven-war-plugin</artifactId>
214214
</plugin>
215-
<!-- Because we are using custom surefire configs in live profile hence need to disable all other in default profile -->
216-
<plugin>
217-
<groupId>org.apache.maven.plugins</groupId>
218-
<artifactId>maven-surefire-plugin</artifactId>
219-
<configuration>
220-
<forkCount>3</forkCount>
221-
<reuseForks>true</reuseForks>
222-
<excludes>
223-
<exclude>**/*IntegrationTest.java</exclude>
224-
<exclude>**/*IntTest.java</exclude>
225-
<exclude>**/*LongRunningUnitTest.java</exclude>
226-
<exclude>**/*ManualTest.java</exclude>
227-
<exclude>**/*LiveTest.java</exclude>
228-
<exclude>**/*TestSuite.java</exclude>
229-
</excludes>
230-
</configuration>
231-
</plugin>
232215
<plugin>
233216
<groupId>org.codehaus.cargo</groupId>
234217
<artifactId>cargo-maven2-plugin</artifactId>
@@ -274,32 +257,6 @@
274257
<id>live</id>
275258
<build>
276259
<plugins>
277-
<plugin>
278-
<groupId>org.apache.maven.plugins</groupId>
279-
<artifactId>maven-surefire-plugin</artifactId>
280-
<executions>
281-
<execution>
282-
<phase>integration-test</phase>
283-
<goals>
284-
<goal>test</goal>
285-
</goals>
286-
<configuration>
287-
<excludes>
288-
<exclude>**/*IntegrationTest.java</exclude>
289-
<exclude>**/*IntTest.java</exclude>
290-
</excludes>
291-
<includes>
292-
<include>**/*LiveTest.java</include>
293-
</includes>
294-
</configuration>
295-
</execution>
296-
</executions>
297-
<configuration>
298-
<systemPropertyVariables>
299-
<test.mime>json</test.mime>
300-
</systemPropertyVariables>
301-
</configuration>
302-
</plugin>
303260
<plugin>
304261
<groupId>org.codehaus.cargo</groupId>
305262
<artifactId>cargo-maven2-plugin</artifactId>

0 commit comments

Comments
 (0)