Skip to content

Commit

Permalink
feature(api-calls): discriminate between actual api error responses a…
Browse files Browse the repository at this point in the history
…nd retrieval errors
  • Loading branch information
lrosenfeldt committed Aug 9, 2024
1 parent bcd030e commit 26fa674
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 11 deletions.
35 changes: 29 additions & 6 deletions src/endpoints/BaseApiClient.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import fetch, { type RequestInit } from 'node-fetch';
import { CommunicatorConfiguration } from '../CommunicatorConfiguration.js';
import { RequestHeaderGenerator } from '../RequestHeaderGenerator.js';
import { ApiErrorResponseException } from '../errors/index.js';
import { ApiErrorResponseException, ApiResponseRetrievalException } from '../errors/index.js';
import type { ErrorResponse } from '../models/ErrorResponse.js';

function isErrorResponse(parsed: object): parsed is ErrorResponse {
const record = parsed as Record<string, unknown>;
if (Object.prototype.hasOwnProperty.call(record, 'errorId') && typeof record['errorId'] !== 'string') {
return false;
}
if (Object.prototype.hasOwnProperty.call(record, 'errorId') && !Array.isArray(record['errors'])) {
return false;
}
return true;
}

export class BaseApiClient {
protected readonly requestHeaderGenerator: RequestHeaderGenerator;
Expand All @@ -25,12 +37,23 @@ export class BaseApiClient {

const response = await fetch(url, requestInit);

const body = await response.json();
const body = await response.text();
let parsed: unknown;
try {
parsed = JSON.parse(body);
} catch (error) {
throw new ApiResponseRetrievalException(response.status, body, error instanceof Error ? error : undefined);
}
if (typeof parsed !== 'object' || parsed === null) {
throw new ApiResponseRetrievalException(response.status, body);
}

if (response.ok) {
return body as Promise<T>;
if (!response.ok) {
if (isErrorResponse(parsed)) {
throw new ApiErrorResponseException(response.status, body, parsed.errors ?? []);
}
throw new ApiResponseRetrievalException(response.status, body);
}
// TODO check if this is a valid error response
throw new ApiErrorResponseException(response.status, JSON.stringify(body));
return parsed as Promise<T>;
}
}
2 changes: 1 addition & 1 deletion src/models/APIError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export interface APIError {
* @description Error code
* @example 50001130
*/
errorCode: string;
errorCode?: string;
/**
* @description Category the error belongs to. The category should give an indication of the type of error you are dealing
* with. Possible values:
Expand Down
17 changes: 14 additions & 3 deletions src/tests/endpoints/CheckoutApiClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { beforeEach, describe, expect, test, vi } from 'vitest';
import { CommunicatorConfiguration } from '../../CommunicatorConfiguration.js';
import type { CheckoutResponse, ErrorResponse } from '../../models/index.js';
import { CheckoutApiClient } from '../../endpoints/CheckoutApiClient.js';
import { createResponseMock } from '../mock-response.js';
import { createResponseMock, createEmptyErrorResponseMock } from '../mock-response.js';
import { ApiErrorResponseException } from '../../errors/ApiErrorResponseException.js';
import { ApiResponseRetrievalException } from '../../errors/ApiResponseRetrievalException.js';

vi.mock('node-fetch', async importOriginal => {
return {
Expand Down Expand Up @@ -34,8 +35,8 @@ describe('CheckoutApiClient', () => {

expect(res).toEqual(expectedResponse);
});
test('given request was not successful, then return errorresponse', async () => {
const expectedResponse: ErrorResponse = {};
test('given request was not successful (400), then return errorresponse', async () => {
const expectedResponse: ErrorResponse = { errorId: 'error-id' };

mockedFetch.mockResolvedValueOnce(createResponseMock<ErrorResponse>(400, expectedResponse));

Expand All @@ -46,5 +47,15 @@ describe('CheckoutApiClient', () => {
expect(error).toEqual(new ApiErrorResponseException(400, JSON.stringify(expectedResponse)));
}
});
test('given request was not successful (500), throw ApiResponseRetrievalException', async () => {
mockedFetch.mockResolvedValueOnce(createEmptyErrorResponseMock(500));

expect.assertions(1);
try {
await checkoutApiClient.createCheckoutRequest('merchantId', 'commerceCaseId', {});
} catch (error) {
expect(error).toEqual(new ApiResponseRetrievalException(500, ''));
}
});
});
});
2 changes: 1 addition & 1 deletion src/tests/mock-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const createResponseMock = <T>(statusCode: number, body?: T) => {
});
};

export const createApiErrorResponseExceptionMock = (statusCode: number, body: string) => {
export const createEmptyErrorResponseMock = (statusCode: number = 500, body: string | undefined = undefined) => {
const usedBody = body ? JSON.stringify(body) : undefined;
return new Response(usedBody, {
status: statusCode,
Expand Down

0 comments on commit 26fa674

Please sign in to comment.