Skip to content

Commit c94f72d

Browse files
authored
Avoid 500 response unless necessary (#509)
* Correct error responses - Move error handling to its own class - Change order so plain text is a fallback - Less logging on user errors - ERROR level logging for INTERNAL_SERVER_ERROR to make these visible - Better test coverage * Just return 404 for unknown paths
1 parent 53d68f8 commit c94f72d

File tree

5 files changed

+164
-104
lines changed

5 files changed

+164
-104
lines changed

klass-api/src/main/java/no/ssb/klass/api/controllers/ClassificationController.java

Lines changed: 1 addition & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
import no.ssb.klass.core.util.AlphaNumericalComparator;
2727
import no.ssb.klass.core.util.ClientException;
2828
import no.ssb.klass.core.util.DateRange;
29-
import no.ssb.klass.core.util.KlassResourceNotFoundException;
3029

3130
import org.slf4j.Logger;
3231
import org.slf4j.LoggerFactory;
@@ -40,14 +39,12 @@
4039
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
4140
import org.springframework.http.HttpStatus;
4241
import org.springframework.http.MediaType;
43-
import org.springframework.http.ProblemDetail;
4442
import org.springframework.http.ResponseEntity;
4543
import org.springframework.transaction.annotation.Transactional;
4644
import org.springframework.transaction.interceptor.TransactionAspectSupport;
4745
import org.springframework.web.bind.WebDataBinder;
4846
import org.springframework.web.bind.annotation.*;
4947
import org.springframework.web.client.RestClientException;
50-
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
5148
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
5249
import org.springframework.web.servlet.view.RedirectView;
5350

@@ -71,8 +68,6 @@ public class ClassificationController {
7168
private final StatisticsService statisticsService;
7269
private final CsvFieldsValidator csvFieldsValidator;
7370

74-
private static final String EXCEPTION_HANDLER_LOG_MESSAGE_TEMPLATE = "{}. For request: {}";
75-
7671
@Autowired
7772
public ClassificationController(
7873
ClassificationService classificationService,
@@ -87,96 +82,6 @@ public ClassificationController(
8782
this.csvFieldsValidator = csvFieldsValidator;
8883
}
8984

90-
@ExceptionHandler(
91-
exception = KlassResourceNotFoundException.class,
92-
produces = {MediaType.TEXT_PLAIN_VALUE, RestConstants.CONTENT_TYPE_CSV})
93-
@ResponseStatus(HttpStatus.NOT_FOUND)
94-
public String resourceNotFoundTextExceptionHandler(KlassResourceNotFoundException exception) {
95-
log.info(
96-
EXCEPTION_HANDLER_LOG_MESSAGE_TEMPLATE,
97-
exception.getMessage(),
98-
getCurrentRequest());
99-
return exception.getMessage();
100-
}
101-
102-
@ExceptionHandler(
103-
exception = KlassResourceNotFoundException.class,
104-
produces = {
105-
MediaType.APPLICATION_JSON_VALUE,
106-
MediaType.APPLICATION_PROBLEM_JSON_VALUE,
107-
MediaType.TEXT_XML_VALUE,
108-
MediaType.APPLICATION_XML_VALUE,
109-
MediaType.APPLICATION_PROBLEM_XML_VALUE
110-
})
111-
@ResponseStatus(HttpStatus.NOT_FOUND)
112-
public ProblemDetail resourceNotFoundProblemDetailExceptionHandler(
113-
KlassResourceNotFoundException exception) {
114-
log.info(
115-
EXCEPTION_HANDLER_LOG_MESSAGE_TEMPLATE,
116-
exception.getMessage(),
117-
getCurrentRequest());
118-
return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, exception.getMessage());
119-
}
120-
121-
@ExceptionHandler(
122-
exception = {
123-
RestClientException.class,
124-
MethodArgumentTypeMismatchException.class,
125-
IllegalArgumentException.class
126-
},
127-
produces = {MediaType.TEXT_PLAIN_VALUE, RestConstants.CONTENT_TYPE_CSV})
128-
@ResponseStatus(HttpStatus.BAD_REQUEST)
129-
public String badRequestTextExceptionHandler(Exception exception) {
130-
return exception.getMessage();
131-
}
132-
133-
@ExceptionHandler(
134-
exception = {
135-
RestClientException.class,
136-
MethodArgumentTypeMismatchException.class,
137-
IllegalArgumentException.class
138-
},
139-
produces = {
140-
MediaType.APPLICATION_JSON_VALUE,
141-
MediaType.APPLICATION_PROBLEM_JSON_VALUE,
142-
MediaType.TEXT_XML_VALUE,
143-
MediaType.APPLICATION_XML_VALUE,
144-
MediaType.APPLICATION_PROBLEM_XML_VALUE
145-
})
146-
@ResponseStatus(HttpStatus.BAD_REQUEST)
147-
public ProblemDetail badRequestProblemDetailExceptionHandler(Exception exception) {
148-
return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, exception.getMessage());
149-
}
150-
151-
@ExceptionHandler
152-
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
153-
public String serverErrorTextExceptionHandler(Exception exception) {
154-
log.warn(
155-
EXCEPTION_HANDLER_LOG_MESSAGE_TEMPLATE,
156-
exception.getMessage(),
157-
getCurrentRequest(),
158-
exception);
159-
return exception.getMessage();
160-
}
161-
162-
@ExceptionHandler(
163-
produces = {
164-
MediaType.APPLICATION_JSON_VALUE,
165-
MediaType.APPLICATION_PROBLEM_JSON_VALUE,
166-
MediaType.TEXT_XML_VALUE,
167-
MediaType.APPLICATION_XML_VALUE,
168-
MediaType.APPLICATION_PROBLEM_XML_VALUE
169-
})
170-
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
171-
public ProblemDetail serverErrorProblemDetailExceptionHandler(Exception exception) {
172-
log.warn(
173-
EXCEPTION_HANDLER_LOG_MESSAGE_TEMPLATE,
174-
exception.getMessage(),
175-
getCurrentRequest(),
176-
exception);
177-
return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, exception.getMessage());
178-
}
179-
18085
/** Redirect root to docs for convenience */
18186
@Hidden // We don't want to document this
18287
@GetMapping("/")
@@ -729,8 +634,7 @@ public CorrespondenceItemList corresponds(
729634
public CorrespondenceItemList correspondsAt(
730635
@PathVariable Long id,
731636
@RequestParam(value = "targetClassificationId") Long targetClassificationId,
732-
@RequestParam(value = "date", required = false)
733-
@DateTimeFormat(pattern = RestConstants.DATE_FORMAT)
637+
@RequestParam(value = "date") @DateTimeFormat(pattern = RestConstants.DATE_FORMAT)
734638
LocalDate date,
735639
@RequestParam(value = "csvSeparator", defaultValue = ",") String csvSeparator,
736640
@RequestParam(value = "csvFields", defaultValue = "") String csvFields,
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package no.ssb.klass.api.controllers.handlers;
2+
3+
import jakarta.servlet.http.HttpServletRequest;
4+
5+
import no.ssb.klass.core.util.KlassResourceNotFoundException;
6+
7+
import org.slf4j.Logger;
8+
import org.slf4j.LoggerFactory;
9+
import org.springframework.http.HttpStatus;
10+
import org.springframework.http.MediaType;
11+
import org.springframework.http.ProblemDetail;
12+
import org.springframework.web.bind.MissingServletRequestParameterException;
13+
import org.springframework.web.bind.annotation.*;
14+
import org.springframework.web.client.RestClientException;
15+
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
16+
import org.springframework.web.servlet.NoHandlerFoundException;
17+
18+
@RestControllerAdvice
19+
public class Handlers {
20+
private static final Logger log = LoggerFactory.getLogger(Handlers.class);
21+
22+
private static final String EXCEPTION_HANDLER_LOG_MESSAGE_TEMPLATE = "{}. For request: {}";
23+
24+
@ExceptionHandler(
25+
exception = {KlassResourceNotFoundException.class, NoHandlerFoundException.class},
26+
produces = {
27+
MediaType.APPLICATION_JSON_VALUE,
28+
MediaType.APPLICATION_PROBLEM_JSON_VALUE,
29+
MediaType.TEXT_XML_VALUE,
30+
MediaType.APPLICATION_XML_VALUE,
31+
MediaType.APPLICATION_PROBLEM_XML_VALUE
32+
})
33+
@ResponseStatus(HttpStatus.NOT_FOUND)
34+
public ProblemDetail resourceNotFoundProblemDetailExceptionHandler(Exception exception) {
35+
if (exception.getClass() == KlassResourceNotFoundException.class) {
36+
return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, exception.getMessage());
37+
}
38+
return ProblemDetail.forStatus(HttpStatus.NOT_FOUND);
39+
}
40+
41+
@ExceptionHandler(
42+
exception = {KlassResourceNotFoundException.class, NoHandlerFoundException.class})
43+
@ResponseStatus(HttpStatus.NOT_FOUND)
44+
public @ResponseBody String resourceNotFoundTextExceptionHandler(Exception exception) {
45+
if (exception.getClass() == KlassResourceNotFoundException.class) {
46+
return exception.getMessage();
47+
}
48+
return "";
49+
}
50+
51+
@ExceptionHandler(
52+
exception = {
53+
RestClientException.class,
54+
MethodArgumentTypeMismatchException.class,
55+
IllegalArgumentException.class,
56+
MissingServletRequestParameterException.class,
57+
java.lang.NumberFormatException.class
58+
},
59+
produces = {
60+
MediaType.APPLICATION_JSON_VALUE,
61+
MediaType.APPLICATION_PROBLEM_JSON_VALUE,
62+
MediaType.TEXT_XML_VALUE,
63+
MediaType.APPLICATION_XML_VALUE,
64+
MediaType.APPLICATION_PROBLEM_XML_VALUE
65+
})
66+
@ResponseStatus(HttpStatus.BAD_REQUEST)
67+
public ProblemDetail badRequestProblemDetailExceptionHandler(Exception exception) {
68+
return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, exception.getMessage());
69+
}
70+
71+
@ExceptionHandler(
72+
exception = {
73+
RestClientException.class,
74+
MethodArgumentTypeMismatchException.class,
75+
IllegalArgumentException.class,
76+
MissingServletRequestParameterException.class
77+
})
78+
@ResponseStatus(HttpStatus.BAD_REQUEST)
79+
public @ResponseBody String badRequestTextExceptionHandler(Exception exception) {
80+
return exception.getMessage();
81+
}
82+
83+
@ExceptionHandler(
84+
produces = {
85+
MediaType.APPLICATION_JSON_VALUE,
86+
MediaType.APPLICATION_PROBLEM_JSON_VALUE,
87+
MediaType.TEXT_XML_VALUE,
88+
MediaType.APPLICATION_XML_VALUE,
89+
MediaType.APPLICATION_PROBLEM_XML_VALUE
90+
})
91+
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
92+
public ProblemDetail serverErrorProblemDetailExceptionHandler(
93+
Exception exception, HttpServletRequest request) {
94+
log.error(
95+
EXCEPTION_HANDLER_LOG_MESSAGE_TEMPLATE,
96+
exception.getMessage(),
97+
request.getRequestURI(),
98+
exception);
99+
return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, exception.getMessage());
100+
}
101+
102+
@ExceptionHandler
103+
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
104+
public String serverErrorTextExceptionHandler(Exception exception, HttpServletRequest request) {
105+
log.error(
106+
EXCEPTION_HANDLER_LOG_MESSAGE_TEMPLATE,
107+
exception.getMessage(),
108+
request.getRequestURI(),
109+
exception);
110+
return exception.getMessage();
111+
}
112+
}

klass-api/src/main/resources/application.properties

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,10 @@ management.metrics.tags.application=klass
4444
klass.env.search.elasticsearch.index=klass
4545
springdoc.swagger-ui.path=${klass.env.api.base.path}/swagger-ui.html
4646
springdoc.api-docs.path=${klass.env.api.base.path}/v3/api-docs
47+
spring.web.resources.add-mappings=false
4748
# Logging properties
48-
logging.level.org.springframework.web.servlet=TRACE
49-
#logging.level.no.ssb.klass=INFO
49+
logging.level.org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping=TRACE
50+
logging.level.no.ssb.klass=INFO
5051
# Mail properties
5152
klass.env.client.klass-mail.url=http://localhost:8082
5253
# hibernate config

klass-api/src/test/java/no/ssb/klass/api/ApiDocumentation.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import no.ssb.klass.api.config.MockConfig;
2323
import no.ssb.klass.api.config.TestConfig;
24+
import no.ssb.klass.api.controllers.handlers.Handlers;
2425
import no.ssb.klass.api.services.OpenSearchResult;
2526
import no.ssb.klass.api.services.SearchService;
2627
import no.ssb.klass.api.util.RestConstants;
@@ -45,6 +46,7 @@
4546
import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration;
4647
import org.springframework.boot.test.context.SpringBootTest;
4748
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
49+
import org.springframework.context.annotation.Import;
4850
import org.springframework.data.domain.Page;
4951
import org.springframework.data.domain.PageImpl;
5052
import org.springframework.data.domain.Pageable;
@@ -94,6 +96,7 @@
9496
@AutoConfigureEmbeddedDatabase(
9597
provider = EMBEDDED,
9698
type = AutoConfigureEmbeddedDatabase.DatabaseType.POSTGRES)
99+
@Import(Handlers.class)
97100
public class ApiDocumentation {
98101
private static final int CLASS_ID_FAMILIEGRUPPERING = 17;
99102
private static final int CLASS_ID_GREENHOUSE_GASES = 84;

klass-api/src/test/java/no/ssb/klass/api/applicationtest/RestApiErrorHandlingIntegrationTest.java

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import static io.restassured.RestAssured.*;
44

5+
import io.restassured.http.ContentType;
6+
57
import no.ssb.klass.api.util.RestConstants;
68

79
import org.junit.jupiter.params.ParameterizedTest;
@@ -12,11 +14,6 @@
1214

1315
import java.util.stream.Stream;
1416

15-
/**
16-
* @author Mads Lundemo, SSB.
17-
* <p>Testsuite that test how the Rest Api handles unexpected sutuations (tests with JSON and
18-
* XML)
19-
*/
2017
class RestApiErrorHandlingIntegrationTest extends AbstractRestApiApplicationTest {
2118

2219
private static Stream<Arguments> contentTypes() {
@@ -27,6 +24,25 @@ private static Stream<Arguments> contentTypes() {
2724
Arguments.of(RestConstants.CONTENT_TYPE_CSV, RestConstants.CONTENT_TYPE_CSV));
2825
}
2926

27+
private static Stream<Arguments> badRequests() {
28+
return Stream.of(
29+
Arguments.of("/api/klass/v1/classifications/139/changes"),
30+
Arguments.of("/api/klass/v1/classifications/2%3Flanguage=en"),
31+
Arguments.of("/api/klass/v1/classifications/null"),
32+
Arguments.of(
33+
"/api/klass/v1/classifications/1/correspondsAt?targetClassificationId=2"),
34+
Arguments.of("/api/klass/v1/classifications/145/variantAt"));
35+
}
36+
37+
private static Stream<Arguments> notFounds() {
38+
return Stream.of(
39+
Arguments.of("/unknown/path"),
40+
Arguments.of("/api/klass/v1/classifications/99999"),
41+
Arguments.of("/api/klass/v1/versions/99999"),
42+
Arguments.of(
43+
"/api/klass/v1/classifications/1/correspondsAt?targetClassificationId=99999&date=2020-01-01"));
44+
}
45+
3046
@ParameterizedTest
3147
@MethodSource("contentTypes")
3248
void classificationNotFound(String acceptedContentType, String responseContentType) {
@@ -56,4 +72,28 @@ void invalidPathParameter(String acceptedContentType, String responseContentType
5672
.statusCode(HttpStatus.BAD_REQUEST.value())
5773
.contentType(responseContentType);
5874
}
75+
76+
@ParameterizedTest
77+
@MethodSource("badRequests")
78+
void badRequests(String path) {
79+
given().port(port)
80+
.contentType(ContentType.JSON)
81+
.when()
82+
.get(path)
83+
.prettyPeek()
84+
.then()
85+
.statusCode(HttpStatus.BAD_REQUEST.value());
86+
}
87+
88+
@ParameterizedTest
89+
@MethodSource("notFounds")
90+
void notFounds(String path) {
91+
given().port(port)
92+
.contentType(ContentType.JSON)
93+
.when()
94+
.get(path)
95+
.prettyPeek()
96+
.then()
97+
.statusCode(HttpStatus.NOT_FOUND.value());
98+
}
5999
}

0 commit comments

Comments
 (0)