Skip to content

Commit 928ce8f

Browse files
authored
Merge pull request #46 from companieshouse/lp-280-add-global-exception-handler
LP-280 Add Global Exception Handler
2 parents 9c07c0e + de17ed5 commit 928ce8f

File tree

5 files changed

+169
-3
lines changed

5 files changed

+169
-3
lines changed

pom.xml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
<jib-maven-plugin.version>3.4.2</jib-maven-plugin.version>
3131
<api-security-java.version>2.0.8</api-security-java.version>
3232
<api-sdk-manager-java-library.version>3.0.6</api-sdk-manager-java-library.version>
33+
<encoder.version>1.3.1</encoder.version>
3334
</properties>
3435

3536
<profiles>
@@ -103,6 +104,11 @@
103104
<artifactId>mapstruct</artifactId>
104105
<version>${org.mapstruct.version}</version>
105106
</dependency>
107+
<dependency>
108+
<groupId>org.owasp.encoder</groupId>
109+
<artifactId>encoder</artifactId>
110+
<version>${encoder.version}</version>
111+
</dependency>
106112

107113
<!-- Test dependencies -->
108114
<dependency>
@@ -164,7 +170,8 @@
164170
<image>416670754337.dkr.ecr.eu-west-2.amazonaws.com/ci-corretto-build-21:latest</image>
165171
</from>
166172
<to>
167-
<image>416670754337.dkr.ecr.eu-west-2.amazonaws.com/local/limited-partnerships-api:latest</image>
173+
<image>416670754337.dkr.ecr.eu-west-2.amazonaws.com/local/limited-partnerships-api:latest
174+
</image>
168175
</to>
169176
</configuration>
170177
</plugin>

src/main/java/uk/gov/companieshouse/limitedpartnershipsapi/controller/PartnershipController.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.springframework.web.bind.annotation.RequestMapping;
1111
import org.springframework.web.bind.annotation.RestController;
1212
import uk.gov.companieshouse.api.model.transaction.Transaction;
13+
import uk.gov.companieshouse.limitedpartnershipsapi.exception.ServiceException;
1314
import uk.gov.companieshouse.limitedpartnershipsapi.model.dto.LimitedPartnershipSubmissionCreatedResponseDto;
1415
import uk.gov.companieshouse.limitedpartnershipsapi.model.dto.LimitedPartnershipSubmissionDto;
1516
import uk.gov.companieshouse.limitedpartnershipsapi.service.LimitedPartnershipService;
@@ -56,7 +57,7 @@ public ResponseEntity<Object> createPartnership(
5657
var response = new LimitedPartnershipSubmissionCreatedResponseDto(submissionId);
5758

5859
return ResponseEntity.created(location).body(response);
59-
} catch (Exception e) {
60+
} catch (ServiceException e) {
6061
ApiLogger.errorContext(requestId, "Error creating Limited Partnership submission", e, logMap);
6162
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
6263
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package uk.gov.companieshouse.limitedpartnershipsapi.exception;
2+
3+
import org.apache.commons.lang3.StringUtils;
4+
import org.apache.commons.lang3.exception.ExceptionUtils;
5+
import org.owasp.encoder.Encode;
6+
import org.springframework.beans.factory.annotation.Value;
7+
import org.springframework.http.HttpStatus;
8+
import org.springframework.http.ResponseEntity;
9+
import org.springframework.web.bind.annotation.ControllerAdvice;
10+
import org.springframework.web.bind.annotation.ExceptionHandler;
11+
import org.springframework.web.context.request.WebRequest;
12+
import uk.gov.companieshouse.limitedpartnershipsapi.utils.ApiLogger;
13+
14+
import java.util.HashMap;
15+
16+
import static uk.gov.companieshouse.limitedpartnershipsapi.utils.Constants.ERIC_REQUEST_ID_KEY;
17+
18+
@ControllerAdvice
19+
public class GlobalExceptionHandler {
20+
21+
/**
22+
* This environment variable enables the length of the log output to be truncated. Useful to prevent flooding of the
23+
* logs by a malicious user, whose text input value may end up being present in an API error message or stack trace.
24+
*/
25+
@Value("${GLOBAL_EXCEPTION_HANDLER_TRUNCATE_LENGTH_CHARS:15000}")
26+
private int truncationLength;
27+
28+
@ExceptionHandler(Exception.class)
29+
public ResponseEntity<Object> handleException(Exception ex, WebRequest webRequest) {
30+
var context = webRequest.getHeader(ERIC_REQUEST_ID_KEY);
31+
var sanitisedExceptionMessage = truncate(Encode.forJava(ex.getMessage()));
32+
var sanitisedStackTrace = truncate(Encode.forJava(ExceptionUtils.getStackTrace(ex)));
33+
var sanitisedRootCause = truncate(Encode.forJava(ExceptionUtils.getStackTrace(ExceptionUtils.getRootCause(ex))));
34+
35+
HashMap<String, Object> logMap = new HashMap<>();
36+
logMap.put("error", ex.getClass());
37+
logMap.put("stackTrace", sanitisedStackTrace);
38+
logMap.put("rootCause", sanitisedRootCause);
39+
40+
ApiLogger.errorContext(context, sanitisedExceptionMessage, null, logMap);
41+
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
42+
}
43+
44+
private String truncate(String input) {
45+
return StringUtils.truncate(input, truncationLength);
46+
}
47+
}

src/test/java/uk/gov/companieshouse/limitedpartnershipsapi/controller/PartnershipControllerTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ void testCreatePartnershipInternalServerError() throws ServiceException {
8383
any(LimitedPartnershipSubmissionDto.class),
8484
eq(REQUEST_ID),
8585
eq(USER_ID)))
86-
.thenThrow(new RuntimeException());
86+
.thenThrow(new ServiceException("TEST"));
8787

8888
var response = partnershipController.createPartnership(
8989
transaction,
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package uk.gov.companieshouse.limitedpartnershipsapi.exception;
2+
3+
import org.junit.jupiter.api.BeforeEach;
4+
import org.junit.jupiter.api.Test;
5+
import org.junit.jupiter.api.extension.ExtendWith;
6+
import org.mockito.ArgumentCaptor;
7+
import org.mockito.Captor;
8+
import org.mockito.Mock;
9+
import org.mockito.MockedStatic;
10+
import org.mockito.junit.jupiter.MockitoExtension;
11+
import org.springframework.http.HttpStatus;
12+
import org.springframework.http.ResponseEntity;
13+
import org.springframework.test.util.ReflectionTestUtils;
14+
import org.springframework.web.context.request.WebRequest;
15+
import uk.gov.companieshouse.limitedpartnershipsapi.utils.ApiLogger;
16+
17+
import java.util.Map;
18+
19+
import static org.junit.jupiter.api.Assertions.assertEquals;
20+
import static org.junit.jupiter.api.Assertions.assertNotNull;
21+
import static org.junit.jupiter.api.Assertions.assertTrue;
22+
import static org.mockito.ArgumentMatchers.eq;
23+
import static org.mockito.Mockito.mockStatic;
24+
import static org.mockito.Mockito.times;
25+
import static org.mockito.Mockito.when;
26+
import static uk.gov.companieshouse.limitedpartnershipsapi.utils.Constants.ERIC_REQUEST_ID_KEY;
27+
28+
@ExtendWith(MockitoExtension.class)
29+
class GlobalExceptionHandlerTest {
30+
31+
private static final String REQUEST_ID = "1234";
32+
private GlobalExceptionHandler globalExceptionHandler;
33+
34+
@Mock
35+
private WebRequest webRequest;
36+
37+
@Captor
38+
private ArgumentCaptor<Map<String, Object>> logMapCaptor;
39+
40+
@BeforeEach
41+
void setUp() {
42+
this.globalExceptionHandler = new GlobalExceptionHandler();
43+
setTruncationLength(1000);
44+
}
45+
46+
@Test
47+
void testHandleExceptionReturnsCorrectResponse() {
48+
when(webRequest.getHeader(ERIC_REQUEST_ID_KEY)).thenReturn(REQUEST_ID);
49+
50+
ResponseEntity<Object> entity = globalExceptionHandler.handleException(new Exception(), webRequest);
51+
52+
assertNotNull(entity);
53+
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, entity.getStatusCode());
54+
}
55+
56+
@Test
57+
void testHandleExceptionEncodesException() {
58+
Throwable rootCause = new Throwable("root cause \n");
59+
Exception exception = new Exception("exception message \n", rootCause);
60+
61+
when(webRequest.getHeader(ERIC_REQUEST_ID_KEY)).thenReturn(REQUEST_ID);
62+
63+
try (MockedStatic<ApiLogger> apiLogger = mockStatic(ApiLogger.class)) {
64+
65+
globalExceptionHandler.handleException(exception, webRequest);
66+
67+
apiLogger.verify(() -> ApiLogger.errorContext(
68+
eq(REQUEST_ID),
69+
eq("exception message \\n"),
70+
eq(null),
71+
logMapCaptor.capture()), times(1));
72+
73+
Map<String, Object> logMap = logMapCaptor.getValue();
74+
String stackTraceString = (String) logMap.get("stackTrace");
75+
assertTrue(stackTraceString.contains("exception message \\n"));
76+
77+
String rootCauseString = (String) logMap.get("rootCause");
78+
assertTrue(rootCauseString.contains("root cause \\n"));
79+
}
80+
}
81+
82+
@Test
83+
void testHandleExceptionTruncatesException() {
84+
setTruncationLength(20);
85+
Throwable rootCause = new Throwable("root cause");
86+
Exception exception = new Exception("12345678901234567890123", rootCause);
87+
88+
when(webRequest.getHeader(ERIC_REQUEST_ID_KEY)).thenReturn(REQUEST_ID);
89+
90+
try (MockedStatic<ApiLogger> apiLogger = mockStatic(ApiLogger.class)) {
91+
globalExceptionHandler.handleException(exception, webRequest);
92+
93+
apiLogger.verify(() -> ApiLogger.errorContext(
94+
eq(REQUEST_ID),
95+
eq("12345678901234567890"),
96+
eq(null),
97+
logMapCaptor.capture()), times(1));
98+
99+
Map<String, Object> logMap = logMapCaptor.getValue();
100+
String stackTraceString = (String) logMap.get("stackTrace");
101+
assertEquals(20, stackTraceString.length());
102+
103+
String rootCauseString = (String) logMap.get("rootCause");
104+
assertEquals(20, rootCauseString.length());
105+
}
106+
}
107+
108+
private void setTruncationLength(int length) {
109+
ReflectionTestUtils.setField(globalExceptionHandler, "truncationLength", length);
110+
}
111+
}

0 commit comments

Comments
 (0)