mirror of https://github.com/actions/toolkit
Handle errors representing non-successful http responses in retry logic
parent
9ad01e4fd3
commit
1ef26b2390
|
@ -13,8 +13,16 @@ async function handleResponse(
|
|||
fail('Retry method called too many times')
|
||||
}
|
||||
|
||||
if (response.statusCode === 999) {
|
||||
// Status codes >= 600 will throw an Error instead of returning a response object. This
|
||||
// mimics the behavior of the http-client *Json methods, which throw an error on any
|
||||
// non-successful status codes. Values in the 6xx, 7xx, and 8xx range are converted
|
||||
// to the corresponding 3xx, 4xx, and 5xx status code.
|
||||
if (response.statusCode >= 900) {
|
||||
throw Error('Test Error')
|
||||
} else if (response.statusCode >= 600) {
|
||||
const error: any = Error('Test Error with Status Code')
|
||||
error['statusCode'] = response.statusCode - 300
|
||||
throw error
|
||||
} else {
|
||||
return Promise.resolve(response)
|
||||
}
|
||||
|
@ -35,6 +43,31 @@ async function testRetryExpectingResult(
|
|||
expect(actualResult.result).toEqual(expectedResult)
|
||||
}
|
||||
|
||||
async function testRetryConvertingErrorToResult(
|
||||
responses: TestResponse[],
|
||||
expectedStatus: number,
|
||||
expectedResult: string | null
|
||||
): Promise<void> {
|
||||
responses = responses.reverse() // Reverse responses since we pop from end
|
||||
|
||||
const actualResult = await retry(
|
||||
'test',
|
||||
async () => handleResponse(responses.pop()),
|
||||
(response: TestResponse) => response.statusCode,
|
||||
2,
|
||||
(e: Error) => {
|
||||
const error: any = e
|
||||
return {
|
||||
statusCode: error['statusCode'],
|
||||
result: error['result']
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
expect(actualResult.statusCode).toEqual(expectedStatus)
|
||||
expect(actualResult.result).toEqual(expectedResult)
|
||||
}
|
||||
|
||||
async function testRetryExpectingError(
|
||||
responses: TestResponse[]
|
||||
): Promise<void> {
|
||||
|
@ -138,3 +171,16 @@ test('retry returns after client error', async () => {
|
|||
null
|
||||
)
|
||||
})
|
||||
|
||||
test('retry converts errors to response object', async () => {
|
||||
await testRetryConvertingErrorToResult(
|
||||
[
|
||||
{
|
||||
statusCode: 709, // throw a 409 Conflict error
|
||||
result: null
|
||||
}
|
||||
],
|
||||
409,
|
||||
null
|
||||
)
|
||||
})
|
||||
|
|
|
@ -35,30 +35,41 @@ export async function retry<T>(
|
|||
name: string,
|
||||
method: () => Promise<T>,
|
||||
getStatusCode: (arg0: T) => number | undefined,
|
||||
maxAttempts = 2
|
||||
maxAttempts = 2,
|
||||
onError: ((arg0: Error) => T | undefined) | undefined = undefined
|
||||
): Promise<T> {
|
||||
let response: T | undefined = undefined
|
||||
let statusCode: number | undefined = undefined
|
||||
let isRetryable = false
|
||||
let errorMessage = ''
|
||||
let attempt = 1
|
||||
|
||||
while (attempt <= maxAttempts) {
|
||||
let response: T | undefined = undefined
|
||||
let statusCode: number | undefined = undefined
|
||||
let isRetryable = false
|
||||
|
||||
try {
|
||||
response = await method()
|
||||
} catch (error) {
|
||||
if (onError) {
|
||||
response = onError(error)
|
||||
}
|
||||
|
||||
isRetryable = true
|
||||
errorMessage = error.message
|
||||
}
|
||||
|
||||
if (response) {
|
||||
statusCode = getStatusCode(response)
|
||||
|
||||
if (!isServerErrorStatusCode(statusCode)) {
|
||||
return response
|
||||
}
|
||||
|
||||
isRetryable = isRetryableStatusCode(statusCode)
|
||||
errorMessage = `Cache service responded with ${statusCode}`
|
||||
} catch (error) {
|
||||
isRetryable = true
|
||||
errorMessage = error.message
|
||||
}
|
||||
|
||||
if (statusCode) {
|
||||
isRetryable = isRetryableStatusCode(statusCode)
|
||||
errorMessage = `Cache service responded with ${statusCode}`
|
||||
}
|
||||
|
||||
core.debug(
|
||||
`${name} - Attempt ${attempt} of ${maxAttempts} failed with error: ${errorMessage}`
|
||||
)
|
||||
|
@ -83,7 +94,22 @@ export async function retryTypedResponse<T>(
|
|||
name,
|
||||
method,
|
||||
(response: ITypedResponse<T>) => response.statusCode,
|
||||
maxAttempts
|
||||
maxAttempts,
|
||||
// If the error object contains the statusCode property, extract it and return
|
||||
// an ITypedResponse<T> so it can be processed by the retry logic. Explicitly
|
||||
// casting Error object to any to workaround missing property errors.
|
||||
(e: Error) => {
|
||||
const error : any = e
|
||||
if (error['statusCode']) {
|
||||
return {
|
||||
statusCode: error['statusCode'],
|
||||
result: null,
|
||||
headers: {}
|
||||
}
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue