Skip to content

Commit

Permalink
Provide more detailed on-chain error messages for failed API calls (#…
Browse files Browse the repository at this point in the history
…1509)

* Use lodash compact instead of .filter(Boolean)

* Clarify message for error in building HTTP request
  • Loading branch information
dcroote authored Oct 27, 2022
1 parent 685a70c commit 5ad00a9
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 8 deletions.
5 changes: 5 additions & 0 deletions .changeset/short-zoos-exercise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@api3/airnode-node': minor
---

Provide more detailed on-chain error messages for failed API calls
30 changes: 26 additions & 4 deletions packages/airnode-node/src/api/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as adapter from '@api3/airnode-adapter';
import { ethers } from 'ethers';
import { AxiosError } from 'axios';
import * as fixtures from '../../test/fixtures';
import { getExpectedTemplateIdV0 } from '../evm/templates';
import { ApiCallErrorResponse, RequestErrorMessage } from '../types';
Expand Down Expand Up @@ -208,20 +209,41 @@ describe('callApi', () => {

it('returns an error if the API call fails to execute', async () => {
const spy = jest.spyOn(adapter, 'buildAndExecuteRequest') as any;
spy.mockRejectedValueOnce(new Error('Network is down'));
const nonAxiosError = new Error('A non-axios error');
spy.mockRejectedValueOnce(nonAxiosError);

const parameters = { _type: 'int256', _path: 'unknown', from: 'ETH' };
const aggregatedApiCall = fixtures.buildAggregatedRegularApiCall({ parameters });
const [logs, res] = await callApi({ type: 'regular', config: fixtures.buildConfig(), aggregatedApiCall });
expect(logs).toEqual([
{ level: 'ERROR', message: 'Failed to call Endpoint:convertToUSD', error: new Error('Network is down') },
]);
expect(logs).toEqual([{ level: 'ERROR', message: 'Failed to call Endpoint:convertToUSD', error: nonAxiosError }]);
expect(res).toEqual({
errorMessage: `${RequestErrorMessage.ApiCallFailed}`,
success: false,
});
});

test.each([
{ e: new AxiosError('Error!', 'CODE', {}, {}, undefined), msg: 'with no response' },
{ e: new AxiosError('Error!', 'CODE', {}, undefined, undefined), msg: 'in building the HTTP request' },
{
e: new AxiosError('Error!', 'CODE', {}, {}, { status: 404, data: {}, statusText: '', headers: {}, config: {} }),
msg: 'with status code 404',
},
])(`returns an error containing "$msg" for the respective axios API call failure`, async ({ e, msg }) => {
const spy = jest.spyOn(adapter, 'buildAndExecuteRequest') as any;
const axiosError = e;
spy.mockRejectedValueOnce(axiosError);

const parameters = { _type: 'int256', _path: 'unknown', from: 'ETH' };
const aggregatedApiCall = fixtures.buildAggregatedRegularApiCall({ parameters });
const [logs, res] = await callApi({ type: 'regular', config: fixtures.buildConfig(), aggregatedApiCall });
expect(logs).toEqual([{ level: 'ERROR', message: 'Failed to call Endpoint:convertToUSD', error: axiosError }]);
expect(res).toEqual({
errorMessage: `${RequestErrorMessage.ApiCallFailed} ${msg}`,
success: false,
});
});

it('returns an error if the value cannot be found with the _path', async () => {
const spy = jest.spyOn(adapter, 'buildAndExecuteRequest') as any;
spy.mockResolvedValueOnce({ data: { price: 1000 } });
Expand Down
19 changes: 17 additions & 2 deletions packages/airnode-node/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import * as adapter from '@api3/airnode-adapter';
import { RESERVED_PARAMETERS } from '@api3/ois';
import { ethers } from 'ethers';
import { logger, removeKeys, removeKey } from '@api3/airnode-utilities';
import { go, goSync } from '@api3/promise-utils';
import axios, { AxiosError } from 'axios';
import { ethers } from 'ethers';
import compact from 'lodash/compact';
import { postProcessApiSpecifications, preProcessApiSpecifications } from './processing';
import { getAirnodeWalletFromPrivateKey, deriveSponsorWalletFromMnemonic } from '../evm';
import { getReservedParameters } from '../adapters/http/parameters';
Expand Down Expand Up @@ -184,6 +186,16 @@ export function isPerformApiCallFailure(
return !!(value as ApiCallErrorResponse).errorMessage;
}

export function errorMsgFromAxiosError(e: AxiosError): string {
if (e.response) {
return `with status code ${e.response.status}`;
} else if (e.request) {
return 'with no response';
} else {
return 'in building the HTTP request';
}
}

export async function performApiCall(
payload: ApiCallPayload
): Promise<LogsData<ApiCallErrorResponse | PerformApiCallSuccess>> {
Expand All @@ -198,7 +210,10 @@ export async function performApiCall(
if (!goRes.success) {
const { aggregatedApiCall } = payload;
const log = logger.pend('ERROR', `Failed to call Endpoint:${aggregatedApiCall.endpointName}`, goRes.error);
return [[log], { success: false, errorMessage: `${RequestErrorMessage.ApiCallFailed}` }];
// eslint-disable-next-line import/no-named-as-default-member
const axiosErrorMsg = axios.isAxiosError(goRes.error) ? errorMsgFromAxiosError(goRes.error) : '';
const errorMessage = compact([RequestErrorMessage.ApiCallFailed, axiosErrorMsg]).join(' ');
return [[log], { success: false, errorMessage: errorMessage }];
}

return [[], { ...goRes.data }];
Expand Down
4 changes: 2 additions & 2 deletions packages/airnode-node/test/e2e/request-status.feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,6 @@ it('sets the correct status code for both successful and failed requests', async
const failedRequest = logs.find(
(log) => log.args.requestId === invalidRequest!.args.requestId && log.name === 'FailedRequest'
);
// The error message will not contain the API error message
expect(failedRequest!.args.errorMessage).toEqual(`${RequestErrorMessage.ApiCallFailed}`);
// The error message will contain the status code, but not the API error message
expect(failedRequest!.args.errorMessage).toEqual(`${RequestErrorMessage.ApiCallFailed} with status code 404`);
});

0 comments on commit 5ad00a9

Please sign in to comment.