Skip to content

Commit dd0be6c

Browse files
authored
Merge pull request #2051 from microsoft/copilot/fix-retry-handler-timeout
fix: prevent negative timeout in RetryHandler causing IllegalArgumentException
2 parents 61b5cba + 57c6753 commit dd0be6c

File tree

2 files changed

+93
-2
lines changed

2 files changed

+93
-2
lines changed

components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/RetryHandler.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public class RetryHandler implements Interceptor {
5959
/**
6060
* One second as milliseconds
6161
*/
62-
private static final long DELAY_MILLISECONDS = 1000;
62+
private static final int DELAY_MILLISECONDS = 1000;
6363

6464
/**
6565
* Initialize retry handler with retry option
@@ -138,7 +138,16 @@ long getRetryAfter(Response response, long delay, int executionCount) {
138138
} else if (retryDelay == -1) {
139139
retryDelay = exponentialBackOffDelay(delay, executionCount);
140140
}
141-
return (long) Math.min(retryDelay, RetryHandlerOption.MAX_DELAY * DELAY_MILLISECONDS);
141+
long result =
142+
(long)
143+
Math.min(
144+
retryDelay,
145+
(double) RetryHandlerOption.MAX_DELAY * DELAY_MILLISECONDS);
146+
// Ensure minimum delay if retry interval is negative
147+
if (result < 0) {
148+
result = 1 + (long) (Math.random() * 9); // Random delay between 1-10ms
149+
}
150+
return result;
142151
}
143152

144153
/**

components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/RetryHandlerTest.java

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
11
package com.microsoft.kiota.http.middleware;
22

33
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertTrue;
5+
import static org.mockito.Mockito.mock;
6+
import static org.mockito.Mockito.when;
7+
8+
import com.microsoft.kiota.http.middleware.options.RetryHandlerOption;
9+
10+
import io.opentelemetry.api.trace.Span;
11+
12+
import okhttp3.Request;
13+
import okhttp3.Response;
414

515
import org.junit.jupiter.api.BeforeEach;
16+
import org.junit.jupiter.api.Test;
617
import org.junit.jupiter.params.ParameterizedTest;
718
import org.junit.jupiter.params.provider.Arguments;
819
import org.junit.jupiter.params.provider.MethodSource;
@@ -58,4 +69,75 @@ void testTryParseTimeHeader(String headerValue, double expectedDelay) {
5869
assertEquals(
5970
expectedDelay, result, DELTA, "Failed for header value: '" + headerValue + "'");
6071
}
72+
73+
@Test
74+
void testGetRetryAfterHandlesNegativeValues() {
75+
// Create a mock response with a Retry-After header that will fail parsing
76+
Response mockResponse = mock(Response.class);
77+
when(mockResponse.header("Retry-After")).thenReturn("invalid");
78+
when(mockResponse.code()).thenReturn(429);
79+
80+
// getRetryAfter should return a value between 1-10ms when parsing fails
81+
long retryAfter = retryHandler.getRetryAfter(mockResponse, 3, 1);
82+
assertTrue(
83+
retryAfter >= 1 && retryAfter <= 10,
84+
"Expected retry interval between 1-10ms, got " + retryAfter);
85+
}
86+
87+
@Test
88+
void testRetryRequestHandlesNegativeRetryInterval() {
89+
// Create mocks
90+
Response mockResponse = mock(Response.class);
91+
Request mockRequest = mock(Request.class);
92+
RetryHandlerOption mockOption = mock(RetryHandlerOption.class);
93+
Span mockSpan = mock(Span.class);
94+
95+
// Setup mock behaviors
96+
when(mockResponse.code()).thenReturn(429);
97+
when(mockResponse.header("Retry-After")).thenReturn("invalid");
98+
when(mockRequest.method()).thenReturn("GET");
99+
when(mockRequest.body()).thenReturn(null);
100+
when(mockOption.maxRetries()).thenReturn(3);
101+
when(mockOption.delay()).thenReturn(3L);
102+
when(mockOption.shouldRetry())
103+
.thenReturn(
104+
(delay, executionCount, request, response) ->
105+
true); // Always retry for this test
106+
107+
// Call retryRequest - it should not throw IllegalArgumentException
108+
// The main goal is to verify no exception is thrown when retry interval is negative
109+
boolean result =
110+
retryHandler.retryRequest(mockResponse, 1, mockRequest, mockOption, mockSpan);
111+
112+
// Verify the method returned true (should retry)
113+
assertTrue(result, "Expected retryRequest to return true");
114+
}
115+
116+
@Test
117+
void testRetryRequestWithNegativeRetryIntervalAppliesRandomDelay() {
118+
// Create mocks
119+
Response mockResponse = mock(Response.class);
120+
Request mockRequest = mock(Request.class);
121+
RetryHandlerOption mockOption = mock(RetryHandlerOption.class);
122+
Span mockSpan = mock(Span.class);
123+
124+
// Setup mock behaviors to force negative retry interval
125+
when(mockResponse.code()).thenReturn(503);
126+
when(mockResponse.header("Retry-After")).thenReturn("invalid");
127+
when(mockRequest.method()).thenReturn("POST");
128+
when(mockRequest.body()).thenReturn(null);
129+
when(mockOption.maxRetries()).thenReturn(5);
130+
when(mockOption.delay()).thenReturn(1L);
131+
when(mockOption.shouldRetry())
132+
.thenReturn((delay, executionCount, request, response) -> true);
133+
134+
// Test multiple times to verify no exception is thrown
135+
// This verifies the fix for negative retry intervals
136+
for (int i = 0; i < 5; i++) {
137+
boolean result =
138+
retryHandler.retryRequest(mockResponse, 1, mockRequest, mockOption, mockSpan);
139+
// Verify each call succeeds without throwing IllegalArgumentException
140+
assertTrue(result, "Expected retryRequest to return true on iteration " + i);
141+
}
142+
}
61143
}

0 commit comments

Comments
 (0)