Skip to content

Commit

Permalink
Allow supplying a retry handler to generated API clients
Browse files Browse the repository at this point in the history
  • Loading branch information
augustuswm committed Sep 20, 2024
1 parent 7890e4d commit b582c0a
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 4 deletions.
35 changes: 33 additions & 2 deletions oxide-api/src/http-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ export interface FullParams extends FetchParams {
method?: string;
}

export type RetryHandler = (
url: RequestInfo | URL,
init: RequestInit,
err: any,
) => boolean;

export interface ApiConfig {
/**
* No host means requests will be sent to the current host. This is used in
Expand All @@ -125,14 +131,21 @@ export interface ApiConfig {
host?: string;
token?: string;
baseParams?: FetchParams;
retryHandler?: RetryHandler;
}

export class HttpClient {
host: string;
token?: string;
baseParams: FetchParams;

constructor({ host = "", baseParams = {}, token }: ApiConfig = {}) {
retryHandler: RetryHandler;

constructor({
host = "",
baseParams = {},
token,
retryHandler,
}: ApiConfig = {}) {
this.host = host;
this.token = token;

Expand All @@ -141,6 +154,7 @@ export class HttpClient {
headers.append("Authorization", `Bearer ${token}`);
}
this.baseParams = mergeParams({ headers }, baseParams);
this.retryHandler = retryHandler ? retryHandler : () => false;
}

public async request<Data>({
Expand All @@ -155,7 +169,24 @@ export class HttpClient {
...mergeParams(this.baseParams, fetchParams),
body: JSON.stringify(snakeify(body), replacer),
};
return fetchWithRetry(fetch, url, init, this.retryHandler);
}
}

export async function fetchWithRetry<Data>(
fetch: any,
url: string,
init: RequestInit,
retry: RetryHandler,
): Promise<ApiResult<Data>> {
try {
return handleResponse(await fetch(url, init));
} catch (err) {
if (retry(url, init, err)) {
return await fetchWithRetry(fetch, url, init, retry);
}

throw err;
}
}

Expand Down
37 changes: 36 additions & 1 deletion oxide-openapi-gen-ts/src/client/static/http-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* Copyright Oxide Computer Company
*/

import { handleResponse, mergeParams } from "./http-client";
import { fetchWithRetry, handleResponse, mergeParams } from "./http-client";
import { describe, expect, it } from "vitest";

const headers = { "Content-Type": "application/json" };
Expand All @@ -17,6 +17,41 @@ const headers = { "Content-Type": "application/json" };
const json = (body: any, status = 200) =>
new Response(JSON.stringify(body), { status, headers });

describe("fetchWithRetry", () => {
it("retries request when handler returns true", async () => {
const retryLimit = 1
let retries = 0
const retryHandler = () => {
if (retries >= retryLimit) {
return false
} else {
retries += 1
return true
}
}

try {
await fetchWithRetry(() => { throw new Error("unimplemented") }, "empty_url", {}, retryHandler)
} catch {
// Throw away any errors we receive, we are only interested in ensuring the retry handler
// gets called and that retries terminate
}

expect(retries).toEqual(1)
});

it("rethrows error when handler returns false", async () => {
const retryHandler = () => false

try {
await fetchWithRetry(() => { throw new Error("unimplemented") }, "empty_url", {}, retryHandler)
throw new Error("Unreachable. This is a bug")
} catch (err: any) {
expect(err.message).toEqual("unimplemented")
}
});
});

describe("handleResponse", () => {
it("handles success", async () => {
const { response, ...rest } = await handleResponse(json({ abc: 123 }));
Expand Down
19 changes: 18 additions & 1 deletion oxide-openapi-gen-ts/src/client/static/http-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ export interface FullParams extends FetchParams {
method?: string;
}

export type RetryHandler = (url: RequestInfo | URL, init: RequestInit, err: any) => boolean;

export interface ApiConfig {
/**
* No host means requests will be sent to the current host. This is used in
Expand All @@ -125,14 +127,16 @@ export interface ApiConfig {
host?: string;
token?: string;
baseParams?: FetchParams;
retryHandler?: RetryHandler;
}

export class HttpClient {
host: string;
token?: string;
baseParams: FetchParams;
retryHandler: RetryHandler;

constructor({ host = "", baseParams = {}, token }: ApiConfig = {}) {
constructor({ host = "", baseParams = {}, token, retryHandler }: ApiConfig = {}) {
this.host = host;
this.token = token;

Expand All @@ -141,6 +145,7 @@ export class HttpClient {
headers.append("Authorization", `Bearer ${token}`);
}
this.baseParams = mergeParams({ headers }, baseParams);
this.retryHandler = retryHandler ? retryHandler : () => false;
}

public async request<Data>({
Expand All @@ -155,7 +160,19 @@ export class HttpClient {
...mergeParams(this.baseParams, fetchParams),
body: JSON.stringify(snakeify(body), replacer),
};
return fetchWithRetry(fetch, url, init, this.retryHandler)
}
}

export async function fetchWithRetry<Data>(fetch: any, url: string, init: RequestInit, retry: RetryHandler): Promise<ApiResult<Data>> {
try {
return handleResponse(await fetch(url, init));
} catch (err) {
if (retry(url, init, err)) {
return await fetchWithRetry(fetch, url, init, retry)
}

throw err
}
}

Expand Down

0 comments on commit b582c0a

Please sign in to comment.