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')
|
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')
|
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 {
|
} else {
|
||||||
return Promise.resolve(response)
|
return Promise.resolve(response)
|
||||||
}
|
}
|
||||||
|
@ -35,6 +43,31 @@ async function testRetryExpectingResult(
|
||||||
expect(actualResult.result).toEqual(expectedResult)
|
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(
|
async function testRetryExpectingError(
|
||||||
responses: TestResponse[]
|
responses: TestResponse[]
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
@ -138,3 +171,16 @@ test('retry returns after client error', async () => {
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('retry converts errors to response object', async () => {
|
||||||
|
await testRetryConvertingErrorToResult(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
statusCode: 709, // throw a 409 Conflict error
|
||||||
|
result: null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
409,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
|
@ -35,28 +35,39 @@ export async function retry<T>(
|
||||||
name: string,
|
name: string,
|
||||||
method: () => Promise<T>,
|
method: () => Promise<T>,
|
||||||
getStatusCode: (arg0: T) => number | undefined,
|
getStatusCode: (arg0: T) => number | undefined,
|
||||||
maxAttempts = 2
|
maxAttempts = 2,
|
||||||
|
onError: ((arg0: Error) => T | undefined) | undefined = undefined
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
let response: T | undefined = undefined
|
|
||||||
let statusCode: number | undefined = undefined
|
|
||||||
let isRetryable = false
|
|
||||||
let errorMessage = ''
|
let errorMessage = ''
|
||||||
let attempt = 1
|
let attempt = 1
|
||||||
|
|
||||||
while (attempt <= maxAttempts) {
|
while (attempt <= maxAttempts) {
|
||||||
|
let response: T | undefined = undefined
|
||||||
|
let statusCode: number | undefined = undefined
|
||||||
|
let isRetryable = false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
response = await method()
|
response = await method()
|
||||||
|
} catch (error) {
|
||||||
|
if (onError) {
|
||||||
|
response = onError(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
isRetryable = true
|
||||||
|
errorMessage = error.message
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response) {
|
||||||
statusCode = getStatusCode(response)
|
statusCode = getStatusCode(response)
|
||||||
|
|
||||||
if (!isServerErrorStatusCode(statusCode)) {
|
if (!isServerErrorStatusCode(statusCode)) {
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusCode) {
|
||||||
isRetryable = isRetryableStatusCode(statusCode)
|
isRetryable = isRetryableStatusCode(statusCode)
|
||||||
errorMessage = `Cache service responded with ${statusCode}`
|
errorMessage = `Cache service responded with ${statusCode}`
|
||||||
} catch (error) {
|
|
||||||
isRetryable = true
|
|
||||||
errorMessage = error.message
|
|
||||||
}
|
}
|
||||||
|
|
||||||
core.debug(
|
core.debug(
|
||||||
|
@ -83,7 +94,22 @@ export async function retryTypedResponse<T>(
|
||||||
name,
|
name,
|
||||||
method,
|
method,
|
||||||
(response: ITypedResponse<T>) => response.statusCode,
|
(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