Skip to content

Commit 671d0a0

Browse files
Brooooooklynclaude
andauthored
Keep Cloudflare calls within the rotation run's time limit
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 90525f3 commit 671d0a0

2 files changed

Lines changed: 28 additions & 2 deletions

File tree

‎platform/packages/api/src/proxy-token-rotation-cloudflare.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ import type { RotationBudget } from './proxy-token-rotation.ts';
1212

1313
const PROXY_TOKEN_BINDING = '__VOID_PROXY_TOKEN';
1414
const REQUEST_TIMEOUT_MS = 60_000;
15+
/**
16+
* A call needs at least this much of the run's time left. Starting one closer
17+
* to the deadline would cut a settings PATCH off with its outcome unknown.
18+
*/
19+
const MIN_REQUEST_MS = 5_000;
1520
const DEFAULT_RETRY_AFTER_SECONDS = 60;
1621
/** Cloudflare's "script not found" error code. */
1722
const SCRIPT_NOT_FOUND_CODE = 10007;
@@ -91,7 +96,8 @@ export function createRotationCloudflareClient(
9196

9297
async function request<T>(method: string, path: string, body?: BodyInit): Promise<T> {
9398
const { budget } = input;
94-
if (budget.cfCallsRemaining <= 0 || Date.now() >= budget.deadline) {
99+
const remainingMs = budget.deadline - Date.now();
100+
if (budget.cfCallsRemaining <= 0 || remainingMs < MIN_REQUEST_MS) {
95101
throw new RotationBudgetExhaustedError();
96102
}
97103
budget.cfCallsRemaining -= 1;
@@ -103,7 +109,7 @@ export function createRotationCloudflareClient(
103109
method,
104110
headers,
105111
body,
106-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
112+
signal: AbortSignal.timeout(Math.min(REQUEST_TIMEOUT_MS, remainingMs)),
107113
});
108114
if (response.status === 429) {
109115
throw new CloudflareRateLimitError(parseRetryAfter(response.headers.get('Retry-After')));

‎platform/packages/api/test/unit/proxy-token-rotation-cloudflare.test.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,4 +175,24 @@ describe('createRotationCloudflareClient', () => {
175175
expect(fetchMock).not.toHaveBeenCalled();
176176
expect(runBudget.cfCallsRemaining).toBe(5);
177177
});
178+
179+
it('stops before fetch when less than five seconds are left', async () => {
180+
const fetchMock = vi.fn<typeof fetch>(async () => cfSuccess(BINDINGS));
181+
const runBudget = { cfCallsRemaining: 5, deadline: Date.now() + 4_000 };
182+
await expect(client(fetchMock, runBudget).getScriptBindings(SCRIPT)).rejects.toBeInstanceOf(
183+
RotationBudgetExhaustedError,
184+
);
185+
expect(fetchMock).not.toHaveBeenCalled();
186+
});
187+
188+
it('times a call out at the run deadline when it comes before the request timeout', async () => {
189+
const timeout = vi.spyOn(AbortSignal, 'timeout');
190+
const fetchMock = vi.fn<typeof fetch>(async () => cfSuccess(BINDINGS));
191+
const runBudget = { cfCallsRemaining: 5, deadline: Date.now() + 20_000 };
192+
await client(fetchMock, runBudget).getScriptBindings(SCRIPT);
193+
const [ms] = timeout.mock.calls[0] ?? [];
194+
expect(ms).toBeGreaterThan(15_000);
195+
expect(ms).toBeLessThanOrEqual(20_000);
196+
timeout.mockRestore();
197+
});
178198
});

0 commit comments

Comments
 (0)