Skip to content

Commit 3bc4a86

Browse files
committed
Add general failure handler
1 parent 29f33a2 commit 3bc4a86

File tree

2 files changed

+353
-3
lines changed

2 files changed

+353
-3
lines changed

src/main/java/com/uid2/optout/vertx/GenericFailureHandler.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,6 @@ public void handle(RoutingContext ctx) {
6161
LOGGER.error("URL: [{}] - Error response code: [{}] - Error:", url, finalStatusCode, t);
6262
} else if (finalStatusCode >= 400 && finalStatusCode < 500) { // 4xx is user error, so just warn
6363
LOGGER.warn("URL: [{}] - Error response code: [{}] - Error:", url, finalStatusCode, t);
64-
} else {
65-
// Status code not in 4xx or 5xx range, log as error
66-
LOGGER.error("URL: [{}] - Unexpected status code: [{}] - Error:", url, finalStatusCode, t);
6764
}
6865

6966
if (!response.ended() && !response.closed()) {
Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
package com.uid2.optout.vertx;
2+
3+
import io.vertx.core.http.HttpClosedException;
4+
import io.vertx.core.http.HttpServerResponse;
5+
import io.vertx.ext.web.RoutingContext;
6+
import org.junit.jupiter.api.BeforeEach;
7+
import org.junit.jupiter.api.Test;
8+
import org.junit.jupiter.api.extension.ExtendWith;
9+
import org.mockito.ArgumentCaptor;
10+
import org.mockito.Mock;
11+
import org.mockito.junit.jupiter.MockitoExtension;
12+
import org.mockito.junit.jupiter.MockitoSettings;
13+
import org.mockito.quality.Strictness;
14+
15+
import static org.junit.jupiter.api.Assertions.*;
16+
import static org.mockito.Mockito.*;
17+
18+
@ExtendWith(MockitoExtension.class)
19+
@MockitoSettings(strictness = Strictness.LENIENT)
20+
public class GenericFailureHandlerTest {
21+
@Mock
22+
private RoutingContext routingContext;
23+
@Mock
24+
private HttpServerResponse response;
25+
26+
private GenericFailureHandler handler;
27+
28+
@BeforeEach
29+
public void setup() {
30+
handler = new GenericFailureHandler();
31+
when(routingContext.response()).thenReturn(response);
32+
when(routingContext.normalizedPath()).thenReturn("/test/path");
33+
when(response.ended()).thenReturn(false);
34+
when(response.closed()).thenReturn(false);
35+
// Mock setStatusCode to return response for method chaining
36+
when(response.setStatusCode(anyInt())).thenReturn(response);
37+
}
38+
39+
@Test
40+
public void testMultipartMethodMismatch_returns400() {
41+
IllegalStateException exception = new IllegalStateException(
42+
"Request method must be one of POST, PUT, PATCH or DELETE to decode a multipart request");
43+
44+
when(routingContext.statusCode()).thenReturn(-1);
45+
when(routingContext.failure()).thenReturn(exception);
46+
47+
handler.handle(routingContext);
48+
49+
ArgumentCaptor<Integer> statusCaptor = ArgumentCaptor.forClass(Integer.class);
50+
ArgumentCaptor<String> bodyCaptor = ArgumentCaptor.forClass(String.class);
51+
52+
verify(response).setStatusCode(statusCaptor.capture());
53+
verify(response).end(bodyCaptor.capture());
54+
55+
assertEquals(400, statusCaptor.getValue());
56+
assertEquals("Bad Request: multipart content not allowed with this HTTP method", bodyCaptor.getValue());
57+
}
58+
59+
@Test
60+
public void testMultipartMethodMismatch_caseInsensitive() {
61+
IllegalStateException exception = new IllegalStateException(
62+
"REQUEST METHOD MUST BE ONE OF POST, PUT, PATCH OR DELETE TO DECODE A MULTIPART REQUEST");
63+
64+
when(routingContext.statusCode()).thenReturn(-1);
65+
when(routingContext.failure()).thenReturn(exception);
66+
67+
handler.handle(routingContext);
68+
69+
ArgumentCaptor<Integer> statusCaptor = ArgumentCaptor.forClass(Integer.class);
70+
verify(response).setStatusCode(statusCaptor.capture());
71+
assertEquals(400, statusCaptor.getValue());
72+
}
73+
74+
@Test
75+
public void testIllegalStateException_withDifferentMessage_handledNormally() {
76+
IllegalStateException exception = new IllegalStateException("Different error message");
77+
78+
when(routingContext.statusCode()).thenReturn(500);
79+
when(routingContext.failure()).thenReturn(exception);
80+
81+
handler.handle(routingContext);
82+
83+
// Should not return 400, but use the status code from context
84+
ArgumentCaptor<Integer> statusCaptor = ArgumentCaptor.forClass(Integer.class);
85+
verify(response).setStatusCode(statusCaptor.capture());
86+
assertEquals(500, statusCaptor.getValue());
87+
}
88+
89+
@Test
90+
public void testMultipartMethodMismatch_withNullMessage_handledNormally() {
91+
IllegalStateException exception = new IllegalStateException((String) null);
92+
93+
when(routingContext.statusCode()).thenReturn(500);
94+
when(routingContext.failure()).thenReturn(exception);
95+
96+
handler.handle(routingContext);
97+
98+
// Should not return 400, but use the status code from context
99+
ArgumentCaptor<Integer> statusCaptor = ArgumentCaptor.forClass(Integer.class);
100+
verify(response).setStatusCode(statusCaptor.capture());
101+
assertEquals(500, statusCaptor.getValue());
102+
}
103+
104+
@Test
105+
public void testTooManyFormFieldsException_withNettyClassNamePattern_returns400() {
106+
// Test with an exception that has a class name matching the Netty pattern
107+
// Actual Netty exception: io.netty.handler.codec.http.multipart.HttpPostRequestDecoder$TooManyFormFieldsException
108+
// The handler checks if className.contains("TooManyFormFieldsException"), so this should work
109+
NettyTooManyFormFieldsException exception = new NettyTooManyFormFieldsException();
110+
111+
when(routingContext.statusCode()).thenReturn(-1);
112+
when(routingContext.failure()).thenReturn(exception);
113+
114+
handler.handle(routingContext);
115+
116+
ArgumentCaptor<Integer> statusCaptor = ArgumentCaptor.forClass(Integer.class);
117+
ArgumentCaptor<String> bodyCaptor = ArgumentCaptor.forClass(String.class);
118+
119+
verify(response).setStatusCode(statusCaptor.capture());
120+
verify(response).end(bodyCaptor.capture());
121+
122+
assertEquals(400, statusCaptor.getValue());
123+
assertEquals("Bad Request: Too many form fields", bodyCaptor.getValue());
124+
125+
// Verify the class name contains "TooManyFormFieldsException" (matching handler logic)
126+
assertTrue(exception.getClass().getName().contains("TooManyFormFieldsException"));
127+
}
128+
129+
@Test
130+
public void testHttpClosedException_ignored() {
131+
HttpClosedException exception = new HttpClosedException("Connection closed");
132+
133+
when(routingContext.statusCode()).thenReturn(-1);
134+
when(routingContext.failure()).thenReturn(exception);
135+
136+
handler.handle(routingContext);
137+
138+
// Should only call end() without setting status code
139+
verify(response, never()).setStatusCode(anyInt());
140+
verify(response).end();
141+
}
142+
143+
@Test
144+
public void testHttpClosedException_responseAlreadyEnded_doesNotCallEnd() {
145+
HttpClosedException exception = new HttpClosedException("Connection closed");
146+
147+
when(routingContext.statusCode()).thenReturn(-1);
148+
when(routingContext.failure()).thenReturn(exception);
149+
when(response.ended()).thenReturn(true);
150+
151+
handler.handle(routingContext);
152+
153+
verify(response, never()).setStatusCode(anyInt());
154+
verify(response, never()).end();
155+
}
156+
157+
@Test
158+
public void testHttpClosedException_responseClosed_doesNotCallEnd() {
159+
HttpClosedException exception = new HttpClosedException("Connection closed");
160+
161+
when(routingContext.statusCode()).thenReturn(-1);
162+
when(routingContext.failure()).thenReturn(exception);
163+
when(response.closed()).thenReturn(true);
164+
165+
handler.handle(routingContext);
166+
167+
verify(response, never()).setStatusCode(anyInt());
168+
verify(response, never()).end();
169+
}
170+
171+
@Test
172+
public void test500StatusCode_logsError() {
173+
RuntimeException exception = new RuntimeException("Internal server error");
174+
175+
when(routingContext.statusCode()).thenReturn(500);
176+
when(routingContext.failure()).thenReturn(exception);
177+
178+
handler.handle(routingContext);
179+
180+
ArgumentCaptor<Integer> statusCaptor = ArgumentCaptor.forClass(Integer.class);
181+
verify(response).setStatusCode(statusCaptor.capture());
182+
assertEquals(500, statusCaptor.getValue());
183+
verify(response).end(anyString());
184+
}
185+
186+
@Test
187+
public void test503StatusCode_logsError() {
188+
RuntimeException exception = new RuntimeException("Service unavailable");
189+
190+
when(routingContext.statusCode()).thenReturn(503);
191+
when(routingContext.failure()).thenReturn(exception);
192+
193+
handler.handle(routingContext);
194+
195+
ArgumentCaptor<Integer> statusCaptor = ArgumentCaptor.forClass(Integer.class);
196+
verify(response).setStatusCode(statusCaptor.capture());
197+
assertEquals(503, statusCaptor.getValue());
198+
verify(response).end(anyString());
199+
}
200+
201+
@Test
202+
public void test400StatusCode_logsWarning() {
203+
IllegalArgumentException exception = new IllegalArgumentException("Bad request");
204+
205+
when(routingContext.statusCode()).thenReturn(400);
206+
when(routingContext.failure()).thenReturn(exception);
207+
208+
handler.handle(routingContext);
209+
210+
ArgumentCaptor<Integer> statusCaptor = ArgumentCaptor.forClass(Integer.class);
211+
verify(response).setStatusCode(statusCaptor.capture());
212+
assertEquals(400, statusCaptor.getValue());
213+
verify(response).end(anyString());
214+
}
215+
216+
@Test
217+
public void test404StatusCode_logsWarning() {
218+
RuntimeException exception = new RuntimeException("Not found");
219+
220+
when(routingContext.statusCode()).thenReturn(404);
221+
when(routingContext.failure()).thenReturn(exception);
222+
223+
handler.handle(routingContext);
224+
225+
ArgumentCaptor<Integer> statusCaptor = ArgumentCaptor.forClass(Integer.class);
226+
verify(response).setStatusCode(statusCaptor.capture());
227+
assertEquals(404, statusCaptor.getValue());
228+
verify(response).end(anyString());
229+
}
230+
231+
@Test
232+
public void testNoStatusCode_defaultsTo500() {
233+
RuntimeException exception = new RuntimeException("Unknown error");
234+
235+
when(routingContext.statusCode()).thenReturn(-1);
236+
when(routingContext.failure()).thenReturn(exception);
237+
238+
handler.handle(routingContext);
239+
240+
ArgumentCaptor<Integer> statusCaptor = ArgumentCaptor.forClass(Integer.class);
241+
verify(response).setStatusCode(statusCaptor.capture());
242+
assertEquals(500, statusCaptor.getValue());
243+
verify(response).end(anyString());
244+
}
245+
246+
@Test
247+
public void testSuccessStatus_status200_responseAlreadyEnded() {
248+
// Sucess status case: Status code is 200 (success) and response is already ended
249+
// The handler should not modify anything since the response is already complete
250+
RuntimeException exception = new RuntimeException("Exception but response already sent");
251+
252+
when(routingContext.statusCode()).thenReturn(200);
253+
when(routingContext.failure()).thenReturn(exception);
254+
when(response.ended()).thenReturn(true); // Response already ended - happy case
255+
256+
handler.handle(routingContext);
257+
258+
// Handler should not modify the response since it's already ended
259+
verify(response, never()).setStatusCode(anyInt());
260+
verify(response, never()).end();
261+
verify(response, never()).end(anyString());
262+
}
263+
264+
@Test
265+
public void testResponseAlreadyEnded_doesNotSetStatusCode() {
266+
RuntimeException exception = new RuntimeException("Error");
267+
268+
when(routingContext.statusCode()).thenReturn(500);
269+
when(routingContext.failure()).thenReturn(exception);
270+
when(response.ended()).thenReturn(true);
271+
272+
handler.handle(routingContext);
273+
274+
verify(response, never()).setStatusCode(anyInt());
275+
verify(response, never()).end();
276+
}
277+
278+
@Test
279+
public void testResponseClosed_doesNotSetStatusCode() {
280+
RuntimeException exception = new RuntimeException("Error");
281+
282+
when(routingContext.statusCode()).thenReturn(500);
283+
when(routingContext.failure()).thenReturn(exception);
284+
when(response.closed()).thenReturn(true);
285+
286+
handler.handle(routingContext);
287+
288+
verify(response, never()).setStatusCode(anyInt());
289+
verify(response, never()).end();
290+
}
291+
292+
@Test
293+
public void testExceptionWithNullMessage() {
294+
// RuntimeException with no message will have getMessage() return null
295+
RuntimeException exception = new RuntimeException((String) null);
296+
297+
when(routingContext.statusCode()).thenReturn(500);
298+
when(routingContext.failure()).thenReturn(exception);
299+
300+
handler.handle(routingContext);
301+
302+
ArgumentCaptor<Integer> statusCaptor = ArgumentCaptor.forClass(Integer.class);
303+
verify(response).setStatusCode(statusCaptor.capture());
304+
assertEquals(500, statusCaptor.getValue());
305+
verify(response).end(anyString());
306+
}
307+
308+
@Test
309+
public void testMultipartMethodMismatch_responseAlreadyEnded_doesNotCallEnd() {
310+
IllegalStateException exception = new IllegalStateException(
311+
"Request method must be one of POST, PUT, PATCH or DELETE to decode a multipart request");
312+
313+
when(routingContext.statusCode()).thenReturn(-1);
314+
when(routingContext.failure()).thenReturn(exception);
315+
when(response.ended()).thenReturn(true);
316+
317+
handler.handle(routingContext);
318+
319+
verify(response, never()).setStatusCode(anyInt());
320+
verify(response, never()).end();
321+
}
322+
323+
@Test
324+
public void testTooManyFormFieldsException_responseAlreadyEnded_doesNotCallEnd() {
325+
TestTooManyFormFieldsException exception = new TestTooManyFormFieldsException("Too many form fields");
326+
327+
when(routingContext.statusCode()).thenReturn(-1);
328+
when(routingContext.failure()).thenReturn(exception);
329+
when(response.ended()).thenReturn(true);
330+
331+
handler.handle(routingContext);
332+
333+
verify(response, never()).setStatusCode(anyInt());
334+
verify(response, never()).end();
335+
}
336+
337+
// Helper class to simulate TooManyFormFieldsException
338+
// The class name must contain "TooManyFormFieldsException" for the handler to recognize it
339+
private static class TestTooManyFormFieldsException extends RuntimeException {
340+
public TestTooManyFormFieldsException(String message) {
341+
super(message);
342+
}
343+
}
344+
345+
// Helper class to simulate the actual Netty exception class name pattern
346+
// This matches: io.netty.handler.codec.http.multipart.HttpPostRequestDecoder$TooManyFormFieldsException
347+
// The $ indicates it's an inner class in Netty
348+
private static class NettyTooManyFormFieldsException extends RuntimeException {
349+
public NettyTooManyFormFieldsException() {
350+
super();
351+
}
352+
}
353+
}

0 commit comments

Comments
 (0)