Skip to content

Commit

Permalink
Shared HTTP client with retries (#5097)
Browse files Browse the repository at this point in the history
  • Loading branch information
kamilkisiela authored Jun 27, 2024
1 parent bc2db5d commit b8998e7
Show file tree
Hide file tree
Showing 15 changed files with 207 additions and 131 deletions.
6 changes: 6 additions & 0 deletions .changeset/good-hairs-deliver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@graphql-hive/apollo': patch
'@graphql-hive/yoga': patch
---

Use built-in retry of http client of the core package
5 changes: 5 additions & 0 deletions .changeset/selfish-shoes-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-hive/cli': patch
---

Retry up to 3 times a GET request in the artifact:fetch command
5 changes: 5 additions & 0 deletions .changeset/tidy-worms-add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-hive/core': minor
---

Add retry mechanism to the http client
71 changes: 32 additions & 39 deletions packages/libraries/apollo/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function createSupergraphSDLFetcher(options: SupergraphSDLFetcherOptions)
? options.endpoint
: joinUrl(options.endpoint, 'supergraph');

return function supergraphSDLFetcher() {
return function supergraphSDLFetcher(): Promise<{ id: string; supergraphSdl: string }> {
const headers: {
[key: string]: string;
} = {
Expand All @@ -41,49 +41,42 @@ export function createSupergraphSDLFetcher(options: SupergraphSDLFetcherOptions)
headers['If-None-Match'] = cacheETag;
}

let retryCount = 0;

const retry = (status: number) => {
if (retryCount >= 10 || status < 499) {
return Promise.reject(new Error(`Failed to fetch [${status}]`));
}

retryCount = retryCount + 1;

return fetchWithRetry();
};

const fetchWithRetry = (): Promise<{ id: string; supergraphSdl: string }> => {
return http
.get(endpoint, {
headers,
})
.then(async response => {
if (response.ok) {
const supergraphSdl = await response.text();
const result = {
id: await createHash('SHA-256').update(supergraphSdl).digest('base64'),
supergraphSdl,
};

const etag = response.headers.get('etag');
if (etag) {
cached = result;
cacheETag = etag;
}
return http
.get(endpoint, {
headers,
retry: {
retryWhen: response => response.status >= 500,
okWhen: response => response.status === 304,
retries: 10,
maxTimeout: 200,
minTimeout: 1,
},
})
.then(async response => {
if (response.ok) {
const supergraphSdl = await response.text();
const result = {
id: await createHash('SHA-256').update(supergraphSdl).digest('base64'),
supergraphSdl,
};

return result;
const etag = response.headers.get('etag');
if (etag) {
cached = result;
cacheETag = etag;
}

if (response.status === 304 && cached !== null) {
return cached;
}
return result;
}

return retry(response.status);
});
};
if (response.status === 304 && cached !== null) {
return cached;
}

return fetchWithRetry();
throw new Error(
`Failed to GET ${endpoint}, received: ${response.status} ${response.statusText ?? 'Internal Server Error'}`,
);
});
};
}

Expand Down
43 changes: 25 additions & 18 deletions packages/libraries/apollo/tests/apollo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import { startStandaloneServer } from '@apollo/server/standalone';
import { http } from '@graphql-hive/core';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { createHive, createSupergraphSDLFetcher, useHive } from '../src';
import { version } from '../src/version';
Expand Down Expand Up @@ -139,21 +140,23 @@ test('should capture client name and version headers', async () => {

await startStandaloneServer(apollo);

await fetch('http://localhost:4000/graphql', {
method: 'POST',
body: JSON.stringify({
await http.post(
'http://localhost:4000/graphql',
JSON.stringify({
query: /* GraphQL */ `
{
hello
}
`,
}),
headers: {
'content-type': 'application/json',
'x-graphql-client-name': 'vitest',
'x-graphql-client-version': '1.0.0',
{
headers: {
'content-type': 'application/json',
'x-graphql-client-name': 'vitest',
'x-graphql-client-version': '1.0.0',
},
},
});
);

await waitFor(50);
await apollo.stop();
Expand Down Expand Up @@ -295,7 +298,9 @@ describe('supergraph SDL fetcher', async () => {
try {
await fetcher();
} catch (err) {
expect(err).toMatchInlineSnapshot(`[Error: Failed to fetch [500]]`);
expect(err).toMatchInlineSnapshot(
`[Error: Failed to fetch http://localhost/supergraph, received: 500 Internal Server Error]`,
);
}
});
});
Expand Down Expand Up @@ -667,17 +672,19 @@ describe('built-in HTTP usage reporting', async () => {
});

(async () => {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-graphql-client-version': '4.2.0',
'x-graphql-client-name': 'apollo-client',
},
body: JSON.stringify({
const response = await http.post(
url,
JSON.stringify({
query: '{hi}',
}),
});
{
headers: {
'Content-Type': 'application/json',
'x-graphql-client-version': '4.2.0',
'x-graphql-client-name': 'apollo-client',
},
},
);

expect(response.status).toBe(200);
expect(await response.json()).toEqual({
Expand Down
1 change: 0 additions & 1 deletion packages/libraries/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@
"@oclif/core": "^3.26.6",
"@oclif/plugin-help": "6.0.22",
"@oclif/plugin-update": "4.2.13",
"@whatwg-node/fetch": "0.9.18",
"colors": "1.4.0",
"env-ci": "7.3.0",
"graphql": "^16.8.1",
Expand Down
14 changes: 8 additions & 6 deletions packages/libraries/cli/src/base-command.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import colors from 'colors';
import { print, type GraphQLError } from 'graphql';
import type { ExecutionResult } from 'graphql';
import { http } from '@graphql-hive/core';
import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
import { Command, Errors, Config as OclifConfig } from '@oclif/core';
import { fetch } from '@whatwg-node/fetch';
import { Config, GetConfigurationValueType, ValidConfigurationKeys } from './helpers/config';

type OmitNever<T> = { [K in keyof T as T[K] extends never ? never : K]: T[K] };
Expand Down Expand Up @@ -166,14 +166,16 @@ export default abstract class extends Command {
operation: TypedDocumentNode<TResult, TVariables>,
...[variables]: TVariables extends Record<string, never> ? [] : [TVariables]
): Promise<TResult> {
const response = await fetch(endpoint, {
headers: requestHeaders,
method: 'POST',
body: JSON.stringify({
const response = await http.post(
endpoint,
JSON.stringify({
query: typeof operation === 'string' ? operation : print(operation),
variables,
}),
});
{
headers: requestHeaders,
},
);

if (!response.ok) {
throw new Error(`Invalid status code for HTTP call: ${response.status}`);
Expand Down
10 changes: 8 additions & 2 deletions packages/libraries/cli/src/commands/artifact/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { http, URL } from '@graphql-hive/core';
import { Flags } from '@oclif/core';
import { fetch, URL } from '@whatwg-node/fetch';
import Command from '../../base-command';

export default class ArtifactsFetch extends Command {
Expand Down Expand Up @@ -40,11 +40,17 @@ export default class ArtifactsFetch extends Command {

const url = new URL(`${cdnEndpoint}/${artifactType}`);

const response = await fetch(url.toString(), {
const response = await http.get(url.toString(), {
headers: {
'x-hive-cdn-key': token,
'User-Agent': `hive-cli/${this.config.version}`,
},
retry: {
retries: 3,
retryWhen(response) {
return response.status >= 500;
},
},
});

if (response.status >= 300) {
Expand Down
1 change: 1 addition & 0 deletions packages/libraries/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"devDependencies": {
"@apollo/federation": "0.38.1",
"@apollo/subgraph": "2.8.1",
"@types/async-retry": "1.4.8",
"@types/lodash.sortby": "4.7.9",
"graphql": "16.9.0",
"nock": "14.0.0-beta.7",
Expand Down
63 changes: 28 additions & 35 deletions packages/libraries/core/src/client/gateways.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,45 +33,38 @@ function createFetcher(options: SchemaFetcherOptions & ServicesFetcherOptions) {
headers['If-None-Match'] = cacheETag;
}

let retryCount = 0;

const retry = (status: number) => {
if (retryCount >= 10 || status < 499) {
return Promise.reject(new Error(`Failed to fetch [${status}]`));
}

retryCount = retryCount + 1;

return fetchWithRetry();
};

const fetchWithRetry = (): Promise<readonly Schema[] | Schema> => {
return http
.get(endpoint, {
headers,
})
.then(async response => {
if (response.ok) {
const result = await response.json();

const etag = response.headers.get('etag');
if (etag) {
cached = result;
cacheETag = etag;
}

return result;
return http
.get(endpoint, {
headers,
retry: {
retryWhen: response => response.status >= 500,
okWhen: response => response.status === 304,
retries: 10,
maxTimeout: 200,
minTimeout: 1,
},
})
.then(async response => {
if (response.ok) {
const result = await response.json();

const etag = response.headers.get('etag');
if (etag) {
cached = result;
cacheETag = etag;
}

if (response.status === 304 && cached !== null) {
return cached;
}
return result;
}

return retry(response.status);
});
};
if (response.status === 304 && cached !== null) {
return cached;
}

return fetchWithRetry();
throw new Error(
`Failed to GET ${endpoint}, received: ${response.status} ${response.statusText ?? 'Internal Server Error'}`,
);
});
};
}

Expand Down
Loading

0 comments on commit b8998e7

Please sign in to comment.