Skip to content

Commit 40db4ca

Browse files
Add 408 timeout error as retryable with idempotency key support
1 parent e0683f1 commit 40db4ca

File tree

4 files changed

+65
-11
lines changed

4 files changed

+65
-11
lines changed

lib/workos/client.rb

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,13 @@ def handle_error_response(response:)
146146
http_status: http_status,
147147
request_id: response['x-request-id'],
148148
)
149+
when 408
150+
raise TimeoutError.new(
151+
message: json['message'],
152+
http_status: http_status,
153+
request_id: response['x-request-id'],
154+
retry_after: response['Retry-After'],
155+
)
149156
when 422
150157
message = json['message']
151158
code = json['code']
@@ -180,7 +187,7 @@ def handle_error_response(response:)
180187
private
181188

182189
def retryable_error?(http_status)
183-
http_status >= 500 || http_status == 429
190+
http_status >= 500 || http_status == 408 || http_status == 429
184191
end
185192

186193
def calculate_backoff(attempt)
@@ -194,8 +201,9 @@ def calculate_backoff(attempt)
194201
end
195202

196203
def calculate_retry_delay(attempt, response)
197-
# If it's a 429 with Retry-After header, use that
198-
if response.code.to_i == 429 && response['Retry-After']
204+
# If it's a 408 or 429 with Retry-After header, use that
205+
http_status = response.code.to_i
206+
if (http_status == 408 || http_status == 429) && response['Retry-After']
199207
retry_after = response['Retry-After'].to_i
200208
return retry_after if retry_after.positive?
201209
end

lib/workos/errors.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def to_s
5050
end
5151

5252
def retryable?
53-
return true if http_status && (http_status >= 500 || http_status == 429)
53+
return true if http_status && (http_status >= 500 || http_status == 408)
5454

5555
false
5656
end

spec/lib/workos/audit_logs_spec.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,48 @@
162162
expect(call_count).to eq(4)
163163
end
164164
end
165+
166+
context 'with 408 request timeout errors' do
167+
before do
168+
WorkOS.config.max_retries = 3
169+
end
170+
171+
after do
172+
WorkOS.config.max_retries = 0
173+
end
174+
175+
it 'retries with the same idempotency key on 408 timeout errors' do
176+
allow(described_class).to receive(:client).and_return(double('client'))
177+
178+
call_count = 0
179+
allow(described_class.client).to receive(:request) do |request|
180+
call_count += 1
181+
expect(request['Idempotency-Key']).to eq('test-idempotency-key')
182+
183+
if call_count < 2
184+
# Return 408 Request Timeout for first attempt
185+
response = double('response', code: '408', body: '{"message": "Request Timeout"}')
186+
allow(response).to receive(:[]).with('x-request-id').and_return('test-request-id')
187+
allow(response).to receive(:[]).with('Retry-After').and_return(nil)
188+
response
189+
else
190+
# Success on 2nd attempt
191+
double('response', code: '201', body: '{}')
192+
end
193+
end
194+
195+
expect(described_class).to receive(:sleep).once
196+
197+
response = described_class.create_event(
198+
organization: 'org_123',
199+
event: valid_event,
200+
idempotency_key: 'test-idempotency-key',
201+
)
202+
203+
expect(response.code).to eq('201')
204+
expect(call_count).to eq(2)
205+
end
206+
end
165207
end
166208
end
167209

spec/lib/workos/client_retry_spec.rb

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,28 +57,28 @@ def self.test_request
5757
end
5858
end
5959

60-
context 'with 429 errors' do
60+
context 'with 408 errors' do
6161
it 'retries with exponential backoff' do
6262
allow(test_module).to receive(:client).and_return(double('client'))
6363
allow(test_module.client).to receive(:request) do
64-
double('response', code: '429', body: '{"message": "Rate Limit Exceeded"}', '[]': nil)
64+
double('response', code: '408', body: '{"message": "Request Timeout"}', '[]': nil)
6565
end
6666

6767
expect(test_module.client).to receive(:request).exactly(4).times
6868
expect(test_module).to receive(:sleep).exactly(3).times
6969

7070
expect do
7171
test_module.test_request
72-
end.to raise_error(WorkOS::RateLimitExceededError)
72+
end.to raise_error(WorkOS::TimeoutError)
7373
end
7474

7575
it 'respects Retry-After header' do
7676
allow(test_module).to receive(:client).and_return(double('client'))
7777

7878
response_with_retry_after = double(
7979
'response',
80-
code: '429',
81-
body: '{"message": "Rate Limit Exceeded"}',
80+
code: '408',
81+
body: '{"message": "Request Timeout"}',
8282
'[]': nil,
8383
)
8484
allow(response_with_retry_after).to receive(:[]).with('Retry-After').and_return('5')
@@ -89,7 +89,7 @@ def self.test_request
8989

9090
expect do
9191
test_module.test_request
92-
end.to raise_error(WorkOS::RateLimitExceededError)
92+
end.to raise_error(WorkOS::TimeoutError)
9393
end
9494
end
9595

@@ -270,11 +270,15 @@ def self.test_request
270270
expect(test_module.send(:retryable_error?, 599)).to eq(true)
271271
end
272272

273+
it 'returns true for 408 errors' do
274+
expect(test_module.send(:retryable_error?, 408)).to eq(true)
275+
end
276+
273277
it 'returns true for 429 errors' do
274278
expect(test_module.send(:retryable_error?, 429)).to eq(true)
275279
end
276280

277-
it 'returns false for 4xx errors (except 429)' do
281+
it 'returns false for 4xx errors (except 408 and 429)' do
278282
expect(test_module.send(:retryable_error?, 400)).to eq(false)
279283
expect(test_module.send(:retryable_error?, 401)).to eq(false)
280284
expect(test_module.send(:retryable_error?, 404)).to eq(false)

0 commit comments

Comments
 (0)