From 7a37a8d13757c00f3965c1c63bbe6eb89c66c337 Mon Sep 17 00:00:00 2001 From: patrick-zippenfenig Date: Wed, 1 Nov 2023 09:27:40 +0100 Subject: [PATCH] fix: add retry and error handling --- src/index.ts | 52 +++++++++++++++++++++++++++++++++++++++------- test/index.spec.ts | 47 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 7 deletions(-) diff --git a/src/index.ts b/src/index.ts index da9515d..a40b7b1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,18 +1,56 @@ import { ByteBuffer } from 'flatbuffers'; import { WeatherApiResponse } from '@openmeteo/sdk/weather-api-response'; +const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)); + +async function fetchRetried( + url: string, + retries = 3, + backoffFactor = 0.5, + backoffMax = 2 +): Promise { + const statusToRetry = [500, 502, 504]; + const statusWithJsonError = [400, 429]; + let currentTry = 0; + let response = await fetch(url); + + while (statusToRetry.includes(response.status)) { + currentTry++; + if (currentTry >= retries) { + throw new Error(response.statusText); + } + const sleepMs = + Math.min(backoffFactor * 2 ** currentTry, backoffMax) * 1000; + await sleep(sleepMs); + response = await fetch(url); + } + + if (statusWithJsonError.includes(response.status)) { + const json = await response.json(); + if ('reason' in json) { + throw new Error((json as { reason: string }).reason); + } + throw new Error(response.statusText); + } + return response; +} + async function fetchWeatherApi( url: string, - params: any + params: any, + retries = 3, + backoffFactor = 0.2, + backoffMax = 2 ): Promise { const urlParams = new URLSearchParams(params); urlParams.set('format', 'flatbuffers'); - //console.log(`${url}?${urlParams.toString()}`); - const response = await fetch(`${url}?${urlParams.toString()}`); - const bb = await response.arrayBuffer(); - const fb = new ByteBuffer(new Uint8Array(bb)); - - // TODO: retry, error handling + const response = await fetchRetried( + `${url}?${urlParams.toString()}`, + retries, + backoffFactor, + backoffMax + ); + const fb = new ByteBuffer(new Uint8Array(await response.arrayBuffer())); const results: WeatherApiResponse[] = []; let pos = 0; diff --git a/test/index.spec.ts b/test/index.spec.ts index 58d7d1f..280a312 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -7,6 +7,7 @@ describe('openmeteo', () => { // 2 location data, with hourly temp and precip global.fetch = jest.fn(() => Promise.resolve({ + status: 200, arrayBuffer: () => Promise.resolve( new Uint8Array([ @@ -92,3 +93,49 @@ describe('openmeteo', () => { }); }); }); + +describe('openmeteo', () => { + describe('client', () => { + test('test_error', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + status: 400, + json: () => Promise.resolve({ reason: 'Some error' }), + }) + ) as jest.Mock; + await expect(fetchWeatherApi('', {})).rejects.toThrow('Some error'); + }); + }); +}); + +describe('openmeteo', () => { + describe('client', () => { + test('test_unknown_error', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + status: 400, + statusText: 'Other error', + json: () => Promise.resolve({}), + }) + ) as jest.Mock; + await expect(fetchWeatherApi('', {})).rejects.toThrow('Other error'); + }); + }); +}); + +describe('openmeteo', () => { + describe('client', () => { + test('test_retry', async () => { + global.fetch = jest.fn(() => + Promise.resolve({ + status: 500, + statusText: 'Internal server error', + }) + ) as jest.Mock; + await expect(fetchWeatherApi('', {})).rejects.toThrow( + 'Internal server error' + ); + expect(fetch).toHaveBeenCalledTimes(3); + }); + }); +});