Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use non-presigned url for S3 HEAD requests #971

Merged
merged 6 commits into from
Jan 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/nine-moose-itch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-hive/client': minor
---

Retry failed requests upon CDN issues.
74 changes: 48 additions & 26 deletions packages/libraries/client/src/apollo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,36 +30,58 @@ export function createSupergraphSDLFetcher({ endpoint, key }: SupergraphSDLFetch
headers['If-None-Match'] = cacheETag;
}

return axios
.get(endpoint + '/supergraph', {
headers,
})
.then(response => {
if (response.status >= 200 && response.status < 300) {
const supergraphSdl = response.data;
const result = {
id: createHash('sha256').update(supergraphSdl).digest('base64'),
supergraphSdl,
};

const etag = response.headers['etag'];
if (etag) {
cached = result;
cacheETag = etag;
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 axios
.get(endpoint + '/supergraph', {
headers,
})
.then(response => {
if (response.status >= 200 && response.status < 300) {
const supergraphSdl = response.data;
const result = {
id: createHash('sha256').update(supergraphSdl).digest('base64'),
supergraphSdl,
};

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

return result;
}

return result;
}
return retry(response.status);
})
.catch(async error => {
if (axios.isAxiosError(error)) {
if (error.response?.status === 304 && cached !== null) {
return cached;
}

return Promise.reject(new Error(`Failed to fetch supergraph [${response.status}]`));
})
.catch(async error => {
if (axios.isAxiosError(error) && error.response?.status === 304 && cached !== null) {
return cached;
}
if (error.response?.status) {
return retry(error.response.status);
}
}

throw error;
});
throw error;
});
};

return fetchWithRetry();
};
}

Expand Down
132 changes: 87 additions & 45 deletions packages/libraries/client/src/gateways.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ interface Schema {
name: string;
}

function createFetcher<T>({ endpoint, key }: SchemaFetcherOptions & ServicesFetcherOptions) {
function createFetcher({ endpoint, key }: SchemaFetcherOptions & ServicesFetcherOptions) {
let cacheETag: string | null = null;
let cached: {
id: string;
supergraphSdl: string;
} | null = null;

return function fetcher(): Promise<T> {
return function fetcher(): Promise<readonly Schema[] | Schema> {
const headers: {
[key: string]: string;
} = {
Expand All @@ -29,64 +29,106 @@ function createFetcher<T>({ endpoint, key }: SchemaFetcherOptions & ServicesFetc
headers['If-None-Match'] = cacheETag;
}

return axios
.get(endpoint + '/services', {
headers,
responseType: 'json',
})
.then(response => {
if (response.status >= 200 && response.status < 300) {
const result = response.data;

const etag = response.headers['etag'];
if (etag) {
cached = result;
cacheETag = etag;
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 axios
.get(endpoint + '/services', {
headers,
responseType: 'json',
})
.then(response => {
if (response.status >= 200 && response.status < 300) {
const result = response.data;

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

return result;
}

return result;
}
return retry(response.status);
})
.catch(async error => {
if (axios.isAxiosError(error)) {
if (error.response?.status === 304 && cached !== null) {
return cached;
}

return Promise.reject(new Error(`Failed to fetch [${response.status}]`));
})
.catch(async error => {
if (axios.isAxiosError(error) && error.response?.status === 304 && cached !== null) {
return cached;
}
if (error.response?.status) {
return retry(error.response.status);
}
}

throw error;
});
};

throw error;
});
return fetchWithRetry();
};
}

export function createSchemaFetcher({ endpoint, key }: SchemaFetcherOptions) {
const fetcher = createFetcher<Schema>({ endpoint, key });
const fetcher = createFetcher({ endpoint, key });

return function schemaFetcher() {
return fetcher().then(schema => ({
id: createHash('sha256')
.update(schema.sdl)
.update(schema.url || '')
.update(schema.name)
.digest('base64'),
...schema,
}));
return fetcher().then(schema => {
let service: Schema;
// Before the new artifacts endpoint the body returned an array or a single object depending on the project type.
// This handles both in a backwards-compatible way.
if (schema instanceof Array) {
if (schema.length !== 1) {
throw new Error(
'Encountered multiple services instead of a single service. Please use createServicesFetcher instead.',
);
}
service = schema[0];
} else {
service = schema;
}

return {
id: createSchemaId(service),
...service,
};
});
};
}

export function createServicesFetcher({ endpoint, key }: ServicesFetcherOptions) {
const fetcher = createFetcher<readonly Schema[]>({ endpoint, key });
const fetcher = createFetcher({ endpoint, key });

return function schemaFetcher() {
return fetcher().then(services =>
services.map(service => ({
id: createHash('sha256')
.update(service.sdl)
.update(service.url || '')
.update(service.name)
.digest('base64'),
...service,
})),
);
return fetcher().then(services => {
if (services instanceof Array) {
return services.map(service => ({
id: createSchemaId(service),
...service,
}));
}
throw new Error(
'Encountered a single service instead of a multiple services. Please use createSchemaFetcher instead.',
);
});
};
}

const createSchemaId = (service: Schema) =>
createHash('sha256')
.update(service.sdl)
.update(service.url || '')
.update(service.name)
.digest('base64');
52 changes: 52 additions & 0 deletions packages/libraries/client/tests/apollo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,55 @@ test('createSupergraphSDLFetcher', async () => {
expect(staleResult.id).toBeDefined();
expect(staleResult.supergraphSdl).toEqual(newSupergraphSdl);
});

test('createSupergraphSDLFetcher retry with unexpected status code (nRetryCount=10)', async () => {
const supergraphSdl = 'type SuperQuery { sdl: String }';
const key = 'secret-key';
nock('http://localhost')
.get('/supergraph')
.times(10)
.reply(500)
.get('/supergraph')
.once()
.matchHeader('X-Hive-CDN-Key', key)
.reply(200, supergraphSdl, {
ETag: 'first',
});

const fetcher = createSupergraphSDLFetcher({
endpoint: 'http://localhost',
key,
});

const result = await fetcher();

expect(result.id).toBeDefined();
expect(result.supergraphSdl).toEqual(supergraphSdl);
});

test('createSupergraphSDLFetcher retry with unexpected status code (nRetryCount=11)', async () => {
expect.assertions(1);
const supergraphSdl = 'type SuperQuery { sdl: String }';
const key = 'secret-key';
nock('http://localhost')
.get('/supergraph')
.times(11)
.reply(500)
.get('/supergraph')
.once()
.matchHeader('X-Hive-CDN-Key', key)
.reply(200, supergraphSdl, {
ETag: 'first',
});

const fetcher = createSupergraphSDLFetcher({
endpoint: 'http://localhost',
key,
});

try {
await fetcher();
} catch (err) {
expect(err).toMatchInlineSnapshot(`[Error: Failed to fetch [500]]`);
}
});
71 changes: 71 additions & 0 deletions packages/libraries/client/tests/gateways.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,74 @@ test('createSchemaFetcher with ETag', async () => {
expect(staleResult.sdl).toEqual(newSchema.sdl);
expect(staleResult.url).toEqual(newSchema.url);
});

test('retry in case of unexpected CDN status code (nRetryCount=10)', async () => {
const schema = {
sdl: 'type Query { noop: String }',
url: 'service-url',
name: 'service-name',
};

const key = 'secret-key';

nock('http://localhost')
.get('/services')
.times(10)
.matchHeader('X-Hive-CDN-Key', key)
.matchHeader('accept', 'application/json')
.reply(500)
.get('/services')
.once()
.matchHeader('X-Hive-CDN-Key', key)
.matchHeader('accept', 'application/json')
.reply(200, schema, {
ETag: 'first',
});

const fetcher = createSchemaFetcher({
endpoint: 'http://localhost',
key,
});

const result = await fetcher();
expect(result.id).toBeDefined();
expect(result.name).toEqual(result.name);
expect(result.sdl).toEqual(result.sdl);
expect(result.url).toEqual(result.url);
});

test('fail in case of unexpected CDN status code (nRetryCount=11)', async () => {
expect.assertions(1);
const schema = {
sdl: 'type Query { noop: String }',
url: 'service-url',
name: 'service-name',
};

const key = 'secret-key';

nock('http://localhost')
.get('/services')
.times(11)
.matchHeader('X-Hive-CDN-Key', key)
.matchHeader('accept', 'application/json')
.reply(500)
.get('/services')
.once()
.matchHeader('X-Hive-CDN-Key', key)
.matchHeader('accept', 'application/json')
.reply(200, schema, {
ETag: 'first',
});

const fetcher = createSchemaFetcher({
endpoint: 'http://localhost',
key,
});

try {
await fetcher();
} catch (e) {
expect(e).toMatchInlineSnapshot(`[Error: Failed to fetch [500]]`);
}
});
Loading