Skip to content

Commit 2ad7077

Browse files
committed
fix: use TotalSeconds/TotalMilliseconds in WaitAndRetry timeout comparison
TimeSpan.Seconds returns only the seconds component (0-59), not the total seconds. For timeouts >= 60 seconds (e.g. the default 60s), .Seconds is 0 because it's exactly 1 minute. This caused WaitAndRetry to always throw a timeout exception instead of retrying, with the misleading message 'The request took longer than the 0 milliseconds allowed'. The fix uses .TotalSeconds and .TotalMilliseconds which return the full value regardless of magnitude. Added regression tests for timeouts >= 60s and correct millisecond reporting in error messages. Bumps version to 3.2.1.
1 parent 0746395 commit 2ad7077

6 files changed

Lines changed: 67 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,12 @@ Fixed handling of No Content responses
234234

235235
- Extend shipment endpoints to request, create, update and delete estimated import charges.
236236

237+
## 3.2.1
238+
239+
### Fixed
240+
241+
- Fixed `WaitAndRetry` using `TimeSpan.Seconds` and `TimeSpan.Milliseconds` (component-only, 0-59 / 0-999) instead of `TimeSpan.TotalSeconds` and `TimeSpan.TotalMilliseconds`. This caused retries to always fail with a spurious timeout exception for any timeout >= 60 seconds (including the default), with the misleading message "The request took longer than the 0 milliseconds allowed".
242+
237243
## 3.2.0
238244

239245
### Changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ var shipengine = new ShipEngine("___YOUR_API_KEY_HERE__");
3636
This C# SDK is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
3737

3838
- API version: 1.1.202603171403
39-
- SDK version: 3.1.0
39+
- SDK version: 3.2.1
4040
- Generator version: 7.7.0
4141
- Build package: org.openapitools.codegen.languages.CSharpClientCodegen
4242
For more information, please visit [https://www.shipengine.com/contact/](https://www.shipengine.com/contact/)

ShipEngineSDK.Test/NetworkTimeoutsTest.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,5 +171,61 @@ public async Task RetryAfterIsGreaterThanTimeoutSetting()
171171
Assert.Equal(ErrorCode.Timeout, ex.ErrorCode);
172172
Assert.Equal("204c855f-dcc0-4270-ba12-c585fc5ef4bf", ex.RequestId);
173173
}
174+
175+
// Regression: TimeSpan.Seconds returns only the seconds component (0-59),
176+
// so a timeout of 60+ seconds would incorrectly compare as 0 and always
177+
// throw a timeout exception instead of retrying.
178+
[Fact]
179+
public async Task RetryWorksWithTimeoutGreaterThanOrEqualTo60Seconds()
180+
{
181+
var config = new Config(apiKey: "TEST_bTYAskEX6tD7vv6u/cZ/M4LaUSWBJ219+8S1jgFcnkk", timeout: TimeSpan.FromSeconds(60), retries: 1);
182+
var mockShipEngineFixture = new MockShipEngineFixture(config);
183+
184+
mockShipEngineFixture.MockHandler.Protected()
185+
.SetupSequence<Task<HttpResponseMessage>>(
186+
"SendAsync",
187+
ItExpr.Is<HttpRequestMessage>(m =>
188+
m.Method == HttpMethod.Put &&
189+
m.RequestUri.AbsolutePath == "/v1/labels/se-1234/void"),
190+
ItExpr.IsAny<CancellationToken>())
191+
.Returns(Task.FromResult(RateLimitResponseMessage))
192+
.Returns(Task.FromResult(
193+
new HttpResponseMessage(HttpStatusCode.OK)
194+
{
195+
Content = new StringContent(VoidLabelResponse)
196+
}
197+
));
198+
199+
// Should retry successfully, not throw a timeout exception
200+
await mockShipEngineFixture.ShipEngine.VoidLabelWithLabelId("se-1234");
201+
202+
mockShipEngineFixture.AssertRequest(HttpMethod.Put, "/v1/labels/se-1234/void", numberOfCalls: 2);
203+
}
204+
205+
[Fact]
206+
public async Task TimeoutMessageShowsCorrectMillisecondsForLargeTimeouts()
207+
{
208+
// RetryAfter header is 1 second; set timeout to 0.5s so it triggers the timeout path
209+
var config = new Config(apiKey: "TEST_bTYAskEX6tD7vv6u/cZ/M4LaUSWBJ219+8S1jgFcnkk", timeout: TimeSpan.FromMilliseconds(1500), retries: 1);
210+
var mockShipEngineFixture = new MockShipEngineFixture(config);
211+
212+
var rateLimitWithHighRetryAfter = new HttpResponseMessage((HttpStatusCode)429);
213+
rateLimitWithHighRetryAfter.Content = new StringContent(rateLimitResponse);
214+
rateLimitWithHighRetryAfter.Headers.Add("RetryAfter", "2");
215+
216+
mockShipEngineFixture.MockHandler.Protected()
217+
.SetupSequence<Task<HttpResponseMessage>>(
218+
"SendAsync",
219+
ItExpr.Is<HttpRequestMessage>(m =>
220+
m.Method == HttpMethod.Put &&
221+
m.RequestUri.AbsolutePath == "/v1/labels/se-1234/void"),
222+
ItExpr.IsAny<CancellationToken>())
223+
.Returns(Task.FromResult(rateLimitWithHighRetryAfter));
224+
225+
var ex = await Assert.ThrowsAsync<ShipEngineException>(async () => await mockShipEngineFixture.ShipEngine.VoidLabelWithLabelId("se-1234"));
226+
227+
Assert.Equal("The request took longer than the 1500 milliseconds allowed", ex.Message);
228+
Assert.Equal(ErrorCode.Timeout, ex.ErrorCode);
229+
}
174230
}
175231
}

ShipEngineSDK/ShipEngineClient.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,10 +300,10 @@ private async Task WaitAndRetry(HttpResponseMessage? response, Config config, Sh
300300
retryAfter = 5;
301301
}
302302

303-
if (config.Timeout.Seconds < retryAfter)
303+
if (config.Timeout.TotalSeconds < retryAfter)
304304
{
305305
throw new ShipEngineException(
306-
$"The request took longer than the {config.Timeout.Milliseconds} milliseconds allowed",
306+
$"The request took longer than the {config.Timeout.TotalMilliseconds} milliseconds allowed",
307307
ErrorSource.Shipengine,
308308
ErrorType.System,
309309
ErrorCode.Timeout,

ShipEngineSDK/ShipEngineSDK.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<PackageId>ShipEngine</PackageId>
55
<PackageTags>sdk;rest;api;shipping;rates;label;tracking;cost;address;validation;normalization;fedex;ups;usps;</PackageTags>
66

7-
<Version>3.2.0</Version>
7+
<Version>3.2.1</Version>
88
<Authors>ShipEngine</Authors>
99
<Company>ShipEngine</Company>
1010
<Summary>The official ShipEngine C# SDK for .NET</Summary>

openapitools.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"ignoreFileOverride": "./.openapi-generator-ignore",
1515
"library": "generichost",
1616
"additionalProperties": {
17-
"packageVersion": "3.2.0",
17+
"packageVersion": "3.2.1",
1818
"targetFramework": "netstandard2.0",
1919
"validatable": false,
2020
"sourceFolder": "",

0 commit comments

Comments
 (0)