feat(core): add retry for transient HTTP and stream-level errors
Add retry mechanism driven by pi settings.json retry.provider config (timeoutMs, maxRetries, maxRetryDelayMs). HTTP-level retries handle 429/5xx with exponential backoff and jitter, respecting Retry-After headers (seconds and HTTP-date formats). Stream-level retries handle cases where the API returns 200 OK but sends an error event in the stream body. Retries only when no content has been emitted yet. Per-attempt timeout via AbortController with automatic retry. Clean abort propagation through the retry loop.
This commit is contained in:
+21
-6
@@ -112,6 +112,7 @@ export function createTestDeps(overrides: Partial<CoreDependencies> = {}): TestD
|
||||
now: () => new Date("2026-05-05T12:00:00Z").getTime(),
|
||||
uuid: () => "00000000-0000-4000-8000-000000000000",
|
||||
cwd: () => "/repo",
|
||||
delay: async () => {},
|
||||
...overrides,
|
||||
})
|
||||
return { streamCommandCode, calculatedUsages }
|
||||
@@ -124,12 +125,15 @@ type SuccessPlan = {
|
||||
chunks?: string[]
|
||||
delays?: number[]
|
||||
hangAfterLast?: boolean
|
||||
/** Delay in ms before the server starts sending the response. */
|
||||
responseDelay?: number
|
||||
}
|
||||
|
||||
type ErrorPlan = {
|
||||
type: "error"
|
||||
status: number
|
||||
body: string
|
||||
headers?: Record<string, string>
|
||||
}
|
||||
|
||||
export type ResponsePlan = SuccessPlan | ErrorPlan
|
||||
@@ -146,6 +150,7 @@ function headersToRecord(headers: IncomingHttpHeaders): Record<string, string> {
|
||||
export interface MockCommandCodeServer {
|
||||
baseUrl(): string
|
||||
mockResponse(plan: ResponsePlan): void
|
||||
mockResponseQueue(plans: ResponsePlan[]): void
|
||||
reset(): void
|
||||
close(): Promise<void>
|
||||
lastRequestBody(): unknown
|
||||
@@ -155,7 +160,7 @@ export interface MockCommandCodeServer {
|
||||
}
|
||||
|
||||
export async function startMockCommandCodeServer(): Promise<MockCommandCodeServer> {
|
||||
let nextPlan: ResponsePlan = { type: "success", events: [] }
|
||||
let planQueue: ResponsePlan[] = [{ type: "success", events: [] }]
|
||||
let lastBody: unknown
|
||||
let lastHeaders: Record<string, string> = {}
|
||||
let requests = 0
|
||||
@@ -183,9 +188,12 @@ export async function startMockCommandCodeServer(): Promise<MockCommandCodeServe
|
||||
lastBody = undefined
|
||||
}
|
||||
|
||||
const plan = nextPlan
|
||||
// Pop the first plan from the queue; keep the last one as fallback.
|
||||
const plan = planQueue.length > 1 ? planQueue.shift()! : planQueue[0]
|
||||
|
||||
if (plan.type === "error") {
|
||||
res.writeHead(plan.status, { "Content-Type": "text/plain" })
|
||||
const headers: Record<string, string> = { "Content-Type": "text/plain", ...plan.headers }
|
||||
res.writeHead(plan.status, headers)
|
||||
res.end(plan.body)
|
||||
return
|
||||
}
|
||||
@@ -223,7 +231,11 @@ export async function startMockCommandCodeServer(): Promise<MockCommandCodeServe
|
||||
}
|
||||
}
|
||||
|
||||
sendNext()
|
||||
if (plan.responseDelay) {
|
||||
setTimeout(sendNext, plan.responseDelay)
|
||||
} else {
|
||||
sendNext()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -238,10 +250,13 @@ export async function startMockCommandCodeServer(): Promise<MockCommandCodeServe
|
||||
return {
|
||||
baseUrl: () => `http://127.0.0.1:${port}`,
|
||||
mockResponse(plan: ResponsePlan) {
|
||||
nextPlan = plan
|
||||
planQueue = [plan]
|
||||
},
|
||||
mockResponseQueue(plans: ResponsePlan[]) {
|
||||
planQueue = [...plans]
|
||||
},
|
||||
reset() {
|
||||
nextPlan = { type: "success", events: [] }
|
||||
planQueue = [{ type: "success", events: [] }]
|
||||
lastBody = undefined
|
||||
lastHeaders = {}
|
||||
requests = 0
|
||||
|
||||
Reference in New Issue
Block a user