Use ETag and If-None-Match in CDN and clients (#427)

* Use ETag and If-None-Match in CDN and clients

* Document it in 'Using registry' chapter
This commit is contained in:
Kamil Kisiela 2022-09-30 13:37:57 +02:00 committed by GitHub
parent 2c5a2896bd
commit 36413f1682
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 601 additions and 30 deletions

View file

@ -7,24 +7,53 @@ import { createHive } from './client';
import { isHiveClient } from './internal/utils';
export function createSupergraphSDLFetcher({ endpoint, key }: SupergraphSDLFetcherOptions) {
let cacheETag: string | null = null;
let cached: {
id: string;
supergraphSdl: string;
} | null = null;
return function supergraphSDLFetcher() {
const headers: {
[key: string]: string;
} = {
'X-Hive-CDN-Key': key,
};
if (cacheETag) {
headers['If-None-Match'] = cacheETag;
}
return axios
.get(endpoint + '/supergraph', {
headers: {
'X-Hive-CDN-Key': key,
},
headers,
})
.then(response => {
if (response.status >= 200 && response.status < 300) {
return response.data;
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 Promise.reject(new Error(`Failed to fetch supergraph [${response.status}]`));
})
.then(supergraphSdl => ({
id: createHash('sha256').update(supergraphSdl).digest('base64'),
supergraphSdl,
}));
.catch(async error => {
if (axios.isAxiosError(error) && error.response?.status === 304 && cached !== null) {
return cached;
}
throw error;
});
};
}

View file

@ -9,21 +9,50 @@ interface Schema {
}
function createFetcher<T>({ endpoint, key }: SchemaFetcherOptions & ServicesFetcherOptions) {
let cacheETag: string | null = null;
let cached: {
id: string;
supergraphSdl: string;
} | null = null;
return function fetcher(): Promise<T> {
const headers: {
[key: string]: string;
} = {
'X-Hive-CDN-Key': key,
accept: 'application/json',
};
if (cacheETag) {
headers['If-None-Match'] = cacheETag;
}
return axios
.get(endpoint + '/schema', {
headers: {
'X-Hive-CDN-Key': key,
accept: 'application/json',
},
headers,
responseType: 'json',
})
.then(response => {
if (response.status >= 200 && response.status < 300) {
return response.data;
const result = response.data;
const etag = response.headers['etag'];
if (etag) {
cached = result;
cacheETag = etag;
}
return result;
}
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;
}
throw error;
});
};
}

View file

@ -2,14 +2,23 @@
import nock from 'nock';
import { createSupergraphSDLFetcher } from '../src/apollo';
test('createSupergraphSDLFetcher', async () => {
test('createSupergraphSDLFetcher without ETag', async () => {
const supergraphSdl = 'type SuperQuery { sdl: String }';
const newSupergraphSdl = 'type NewSuperQuery { sdl: String }';
const key = 'secret-key';
nock('http://localhost')
.get('/supergraph')
.once()
.matchHeader('X-Hive-CDN-Key', key)
.reply(() => [200, supergraphSdl]);
.reply(200, supergraphSdl, {
ETag: 'first',
})
.get('/supergraph')
.once()
.matchHeader('X-Hive-CDN-Key', key)
.reply(200, newSupergraphSdl, {
ETag: 'second',
});
const fetcher = createSupergraphSDLFetcher({
endpoint: 'http://localhost',
@ -20,4 +29,53 @@ test('createSupergraphSDLFetcher', async () => {
expect(result.id).toBeDefined();
expect(result.supergraphSdl).toEqual(supergraphSdl);
const secondResult = await fetcher();
expect(secondResult.id).toBeDefined();
expect(secondResult.supergraphSdl).toEqual(newSupergraphSdl);
});
test('createSupergraphSDLFetcher', async () => {
const supergraphSdl = 'type SuperQuery { sdl: String }';
const newSupergraphSdl = 'type Query { sdl: String }';
const key = 'secret-key';
nock('http://localhost')
.get('/supergraph')
.once()
.matchHeader('X-Hive-CDN-Key', key)
.reply(200, supergraphSdl, {
ETag: 'first',
})
.get('/supergraph')
.once()
.matchHeader('X-Hive-CDN-Key', key)
.matchHeader('If-None-Match', 'first')
.reply(304)
.get('/supergraph')
.matchHeader('X-Hive-CDN-Key', key)
.matchHeader('If-None-Match', 'first')
.reply(200, newSupergraphSdl, {
ETag: 'changed',
});
const fetcher = createSupergraphSDLFetcher({
endpoint: 'http://localhost',
key,
});
const result = await fetcher();
expect(result.id).toBeDefined();
expect(result.supergraphSdl).toEqual(supergraphSdl);
const cachedResult = await fetcher();
expect(cachedResult.id).toBeDefined();
expect(cachedResult.supergraphSdl).toEqual(supergraphSdl);
const staleResult = await fetcher();
expect(staleResult.id).toBeDefined();
expect(staleResult.supergraphSdl).toEqual(newSupergraphSdl);
});

View file

@ -6,19 +6,29 @@ afterEach(() => {
nock.cleanAll();
});
test('createServicesFetcher', async () => {
test('createServicesFetcher without ETag', async () => {
const schema = {
sdl: 'type Query { noop: String }',
url: 'service-url',
name: 'service-name',
};
const newSchema = {
sdl: 'type NewQuery { noop: String }',
url: 'new-service-url',
name: 'new-service-name',
};
const key = 'secret-key';
nock('http://localhost')
.get('/schema')
.once()
.matchHeader('X-Hive-CDN-Key', key)
.matchHeader('accept', 'application/json')
.reply(() => [200, [schema]]);
.reply(() => [200, [schema]])
.get('/schema')
.once()
.matchHeader('X-Hive-CDN-Key', key)
.matchHeader('accept', 'application/json')
.reply(() => [200, [newSchema]]);
const fetcher = createServicesFetcher({
endpoint: 'http://localhost',
@ -32,21 +42,104 @@ test('createServicesFetcher', async () => {
expect(result[0].name).toEqual(schema.name);
expect(result[0].sdl).toEqual(schema.sdl);
expect(result[0].url).toEqual(schema.url);
const secondResult = await fetcher();
expect(secondResult).toHaveLength(1);
expect(secondResult[0].id).toBeDefined();
expect(secondResult[0].name).toEqual(newSchema.name);
expect(secondResult[0].sdl).toEqual(newSchema.sdl);
expect(secondResult[0].url).toEqual(newSchema.url);
});
test('createSchemaFetcher', async () => {
test('createServicesFetcher with ETag', async () => {
const schema = {
sdl: 'type Query { noop: String }',
url: 'service-url',
name: 'service-name',
};
const newSchema = {
sdl: 'type NewQuery { noop: String }',
url: 'new-service-url',
name: 'new-service-name',
};
const key = 'secret-key';
nock('http://localhost')
.get('/schema')
.once()
.matchHeader('X-Hive-CDN-Key', key)
.matchHeader('accept', 'application/json')
.reply(() => [200, schema]);
.reply(200, [schema], {
ETag: 'first',
})
.get('/schema')
.once()
.matchHeader('X-Hive-CDN-Key', key)
.matchHeader('accept', 'application/json')
.matchHeader('If-None-Match', 'first')
.reply(304)
.get('/schema')
.once()
.matchHeader('X-Hive-CDN-Key', key)
.matchHeader('accept', 'application/json')
.matchHeader('If-None-Match', 'first')
.reply(200, [newSchema], {
ETag: 'changed',
});
const fetcher = createServicesFetcher({
endpoint: 'http://localhost',
key,
});
const firstResult = await fetcher();
expect(firstResult).toHaveLength(1);
expect(firstResult[0].id).toBeDefined();
expect(firstResult[0].name).toEqual(schema.name);
expect(firstResult[0].sdl).toEqual(schema.sdl);
expect(firstResult[0].url).toEqual(schema.url);
const secondResult = await fetcher();
expect(secondResult).toHaveLength(1);
expect(secondResult[0].id).toBeDefined();
expect(secondResult[0].name).toEqual(schema.name);
expect(secondResult[0].sdl).toEqual(schema.sdl);
expect(secondResult[0].url).toEqual(schema.url);
const staleResult = await fetcher();
expect(staleResult).toHaveLength(1);
expect(staleResult[0].id).toBeDefined();
expect(staleResult[0].name).toEqual(newSchema.name);
expect(staleResult[0].sdl).toEqual(newSchema.sdl);
expect(staleResult[0].url).toEqual(newSchema.url);
});
test('createSchemaFetcher without ETag (older versions)', async () => {
const schema = {
sdl: 'type Query { noop: String }',
url: 'service-url',
name: 'service-name',
};
const newSchema = {
sdl: 'type NewQuery { noop: String }',
url: 'new-service-url',
name: 'new-service-name',
};
const key = 'secret-key';
nock('http://localhost')
.get('/schema')
.once()
.matchHeader('X-Hive-CDN-Key', key)
.matchHeader('accept', 'application/json')
.reply(() => [200, schema])
.get('/schema')
.once()
.matchHeader('X-Hive-CDN-Key', key)
.matchHeader('accept', 'application/json')
.reply(() => [200, newSchema]);
const fetcher = createSchemaFetcher({
endpoint: 'http://localhost',
@ -59,4 +152,73 @@ test('createSchemaFetcher', async () => {
expect(result.name).toEqual(schema.name);
expect(result.sdl).toEqual(schema.sdl);
expect(result.url).toEqual(schema.url);
const newResult = await fetcher();
expect(newResult.id).toBeDefined();
expect(newResult.name).toEqual(newSchema.name);
expect(newResult.sdl).toEqual(newSchema.sdl);
expect(newResult.url).toEqual(newSchema.url);
});
test('createSchemaFetcher with ETag', async () => {
const schema = {
sdl: 'type Query { noop: String }',
url: 'service-url',
name: 'service-name',
};
const newSchema = {
sdl: 'type NewQuery { noop: String }',
url: 'new-service-url',
name: 'new-service-name',
};
const key = 'secret-key';
nock('http://localhost')
.get('/schema')
.once()
.matchHeader('X-Hive-CDN-Key', key)
.matchHeader('accept', 'application/json')
.reply(200, schema, {
ETag: 'first',
})
.get('/schema')
.once()
.matchHeader('X-Hive-CDN-Key', key)
.matchHeader('accept', 'application/json')
.matchHeader('If-None-Match', 'first')
.reply(304)
.get('/schema')
.once()
.matchHeader('X-Hive-CDN-Key', key)
.matchHeader('accept', 'application/json')
.matchHeader('If-None-Match', 'first')
.reply(200, newSchema, {
ETag: 'changed',
});
const fetcher = createSchemaFetcher({
endpoint: 'http://localhost',
key,
});
const firstResult = await fetcher();
expect(firstResult.id).toBeDefined();
expect(firstResult.name).toEqual(schema.name);
expect(firstResult.sdl).toEqual(schema.sdl);
expect(firstResult.url).toEqual(schema.url);
const secondResult = await fetcher();
expect(secondResult.id).toBeDefined();
expect(secondResult.name).toEqual(schema.name);
expect(secondResult.sdl).toEqual(schema.sdl);
expect(secondResult.url).toEqual(schema.url);
const staleResult = await fetcher();
expect(staleResult.id).toBeDefined();
expect(staleResult.name).toEqual(newSchema.name);
expect(staleResult.sdl).toEqual(newSchema.sdl);
expect(staleResult.url).toEqual(newSchema.url);
});

View file

@ -9,6 +9,14 @@ import {
import { isKeyValid } from './auth';
import { buildSchema, introspectionFromSchema } from 'graphql';
async function createETag(value: string) {
const myText = new TextEncoder().encode(value);
const myDigest = await crypto.subtle.digest({ name: 'SHA-256' }, myText);
const hashArray = Array.from(new Uint8Array(myDigest));
return `"${hashArray.map(b => b.toString(16).padStart(2, '0')).join('')}"`;
}
type SchemaArtifact = {
sdl: string;
url?: string;
@ -20,37 +28,50 @@ const artifactTypesHandlers = {
/**
* Returns SchemaArtifact or SchemaArtifact[], same way as it's stored in the storage
*/
schema: (targetId: string, artifactType: string, rawValue: string) =>
schema: (targetId: string, artifactType: string, rawValue: string, etag: string) =>
new Response(rawValue, {
status: 200,
headers: {
'Content-Type': 'application/json',
etag,
},
}),
/**
* Returns Federation Supergraph, we store it as-is.
*/
supergraph: (targetId: string, artifactType: string, rawValue: string) => new Response(rawValue, { status: 200 }),
sdl: (targetId: string, artifactType: string, rawValue: string) => {
supergraph: (targetId: string, artifactType: string, rawValue: string, etag: string) =>
new Response(rawValue, {
status: 200,
headers: {
etag,
},
}),
sdl: (targetId: string, artifactType: string, rawValue: string, etag: string) => {
if (rawValue.startsWith('[')) {
return new InvalidArtifactMatch(artifactType, targetId);
}
const parsed = JSON.parse(rawValue) as SchemaArtifact;
return new Response(parsed.sdl, { status: 200 });
return new Response(parsed.sdl, {
status: 200,
headers: {
etag,
},
});
},
/**
* Returns Metadata same way as it's stored in the storage
*/
metadata: (targetId: string, artifactType: string, rawValue: string) =>
metadata: (targetId: string, artifactType: string, rawValue: string, etag: string) =>
new Response(rawValue, {
status: 200,
headers: {
'Content-Type': 'application/json',
etag,
},
}),
introspection: (targetId: string, artifactType: string, rawValue: string) => {
introspection: (targetId: string, artifactType: string, rawValue: string, etag: string) => {
if (rawValue.startsWith('[')) {
return new InvalidArtifactMatch(artifactType, targetId);
}
@ -64,6 +85,7 @@ const artifactTypesHandlers = {
status: 200,
headers: {
'Content-Type': 'application/json',
etag,
},
});
},
@ -142,17 +164,24 @@ export async function handleRequest(request: Request, keyValidator: typeof isKey
const rawValue = await HIVE_DATA.get(kvStorageKey);
if (rawValue) {
const etag = await createETag(`${kvStorageKey}|${rawValue}`);
const ifNoneMatch = request.headers.get('if-none-match');
if (ifNoneMatch && ifNoneMatch === etag) {
return new Response(null, { status: 304 });
}
switch (artifactType) {
case 'schema':
return artifactTypesHandlers.schema(targetId, artifactType, rawValue);
return artifactTypesHandlers.schema(targetId, artifactType, rawValue, etag);
case 'supergraph':
return artifactTypesHandlers.supergraph(targetId, artifactType, rawValue);
return artifactTypesHandlers.supergraph(targetId, artifactType, rawValue, etag);
case 'sdl':
return artifactTypesHandlers.sdl(targetId, artifactType, rawValue);
return artifactTypesHandlers.sdl(targetId, artifactType, rawValue, etag);
case 'introspection':
return artifactTypesHandlers.introspection(targetId, artifactType, rawValue);
return artifactTypesHandlers.introspection(targetId, artifactType, rawValue, etag);
case 'metadata':
return artifactTypesHandlers.metadata(targetId, artifactType, rawValue);
return artifactTypesHandlers.metadata(targetId, artifactType, rawValue, etag);
default:
return new Response(null, {
status: 500,

View file

@ -69,6 +69,256 @@ describe('CDN Worker', () => {
expect(metadataResponse.headers.get('content-type')).toBe('application/json');
});
test('etag + if-none-match for schema', async () => {
const SECRET = '123456';
const targetId = 'fake-target-id';
const map = new Map();
map.set(`target:${targetId}:schema`, JSON.stringify({ sdl: `type Query { dummy: String }` }));
mockWorkerEnv({
HIVE_DATA: map,
KEY_DATA: SECRET,
});
const token = createToken(SECRET, targetId);
const firstRequest = new Request(`https://fake-worker.com/${targetId}/schema`, {
headers: {
'x-hive-cdn-key': token,
},
});
const firstResponse = await handleRequest(firstRequest, KeyValidators.Bcrypt);
const etag = firstResponse.headers.get('etag');
expect(firstResponse.status).toBe(200);
expect(firstResponse.body).toBeDefined();
// Every request receives the etag
expect(etag).toBeDefined();
// Sending the etag in the if-none-match header should result in a 304
const secondRequest = new Request(`https://fake-worker.com/${targetId}/schema`, {
headers: {
'x-hive-cdn-key': token,
'if-none-match': etag!,
},
});
const secondResponse = await handleRequest(secondRequest, KeyValidators.Bcrypt);
expect(secondResponse.status).toBe(304);
expect(secondResponse.body).toBeNull();
// Sending the etag in the if-none-match header should result in a 304
const wrongEtagRequest = new Request(`https://fake-worker.com/${targetId}/schema`, {
headers: {
'x-hive-cdn-key': token,
'if-none-match': '"non-existing-etag"',
},
});
const wrongEtagResponse = await handleRequest(wrongEtagRequest, KeyValidators.Bcrypt);
expect(wrongEtagResponse.status).toBe(200);
expect(wrongEtagResponse.body).toBeDefined();
});
test('etag + if-none-match for supergraph', async () => {
const SECRET = '123456';
const targetId = 'fake-target-id';
const map = new Map();
map.set(`target:${targetId}:supergraph`, JSON.stringify({ sdl: `type Query { dummy: String }` }));
mockWorkerEnv({
HIVE_DATA: map,
KEY_DATA: SECRET,
});
const token = createToken(SECRET, targetId);
const firstRequest = new Request(`https://fake-worker.com/${targetId}/supergraph`, {
headers: {
'x-hive-cdn-key': token,
},
});
const firstResponse = await handleRequest(firstRequest, KeyValidators.Bcrypt);
const etag = firstResponse.headers.get('etag');
expect(firstResponse.status).toBe(200);
expect(firstResponse.body).toBeDefined();
// Every request receives the etag
expect(etag).toBeDefined();
// Sending the etag in the if-none-match header should result in a 304
const secondRequest = new Request(`https://fake-worker.com/${targetId}/supergraph`, {
headers: {
'x-hive-cdn-key': token,
'if-none-match': etag!,
},
});
const secondResponse = await handleRequest(secondRequest, KeyValidators.Bcrypt);
expect(secondResponse.status).toBe(304);
expect(secondResponse.body).toBeNull();
// Sending the etag in the if-none-match header should result in a 304
const wrongEtagRequest = new Request(`https://fake-worker.com/${targetId}/supergraph`, {
headers: {
'x-hive-cdn-key': token,
'if-none-match': '"non-existing-etag"',
},
});
const wrongEtagResponse = await handleRequest(wrongEtagRequest, KeyValidators.Bcrypt);
expect(wrongEtagResponse.status).toBe(200);
expect(wrongEtagResponse.body).toBeDefined();
});
test('etag + if-none-match for metadata', async () => {
const SECRET = '123456';
const targetId = 'fake-target-id';
const map = new Map();
map.set(`target:${targetId}:metadata`, JSON.stringify({ sdl: `type Query { dummy: String }` }));
mockWorkerEnv({
HIVE_DATA: map,
KEY_DATA: SECRET,
});
const token = createToken(SECRET, targetId);
const firstRequest = new Request(`https://fake-worker.com/${targetId}/metadata`, {
headers: {
'x-hive-cdn-key': token,
},
});
const firstResponse = await handleRequest(firstRequest, KeyValidators.Bcrypt);
const etag = firstResponse.headers.get('etag');
expect(firstResponse.status).toBe(200);
expect(firstResponse.body).toBeDefined();
// Every request receives the etag
expect(etag).toBeDefined();
// Sending the etag in the if-none-match header should result in a 304
const secondRequest = new Request(`https://fake-worker.com/${targetId}/metadata`, {
headers: {
'x-hive-cdn-key': token,
'if-none-match': etag!,
},
});
const secondResponse = await handleRequest(secondRequest, KeyValidators.Bcrypt);
expect(secondResponse.status).toBe(304);
expect(secondResponse.body).toBeNull();
// Sending the etag in the if-none-match header should result in a 304
const wrongEtagRequest = new Request(`https://fake-worker.com/${targetId}/metadata`, {
headers: {
'x-hive-cdn-key': token,
'if-none-match': '"non-existing-etag"',
},
});
const wrongEtagResponse = await handleRequest(wrongEtagRequest, KeyValidators.Bcrypt);
expect(wrongEtagResponse.status).toBe(200);
expect(wrongEtagResponse.body).toBeDefined();
});
test('etag + if-none-match for introspection', async () => {
const SECRET = '123456';
const targetId = 'fake-target-id';
const map = new Map();
map.set(`target:${targetId}:schema`, JSON.stringify({ sdl: `type Query { dummy: String }` }));
mockWorkerEnv({
HIVE_DATA: map,
KEY_DATA: SECRET,
});
const token = createToken(SECRET, targetId);
const firstRequest = new Request(`https://fake-worker.com/${targetId}/introspection`, {
headers: {
'x-hive-cdn-key': token,
},
});
const firstResponse = await handleRequest(firstRequest, KeyValidators.Bcrypt);
const etag = firstResponse.headers.get('etag');
expect(firstResponse.status).toBe(200);
expect(firstResponse.body).toBeDefined();
// Every request receives the etag
expect(etag).toBeDefined();
// Sending the etag in the if-none-match header should result in a 304
const secondRequest = new Request(`https://fake-worker.com/${targetId}/introspection`, {
headers: {
'x-hive-cdn-key': token,
'if-none-match': etag!,
},
});
const secondResponse = await handleRequest(secondRequest, KeyValidators.Bcrypt);
expect(secondResponse.status).toBe(304);
expect(secondResponse.body).toBeNull();
// Sending the etag in the if-none-match header should result in a 304
const wrongEtagRequest = new Request(`https://fake-worker.com/${targetId}/introspection`, {
headers: {
'x-hive-cdn-key': token,
'if-none-match': '"non-existing-etag"',
},
});
const wrongEtagResponse = await handleRequest(wrongEtagRequest, KeyValidators.Bcrypt);
expect(wrongEtagResponse.status).toBe(200);
expect(wrongEtagResponse.body).toBeDefined();
});
test('etag + if-none-match for sdl', async () => {
const SECRET = '123456';
const targetId = 'fake-target-id';
const map = new Map();
map.set(
`target:${targetId}:schema`,
JSON.stringify({
sdl: `type Query { dummy: String }`,
})
);
mockWorkerEnv({
HIVE_DATA: map,
KEY_DATA: SECRET,
});
const token = createToken(SECRET, targetId);
const firstRequest = new Request(`https://fake-worker.com/${targetId}/sdl`, {
headers: {
'x-hive-cdn-key': token,
},
});
const firstResponse = await handleRequest(firstRequest, KeyValidators.Bcrypt);
const etag = firstResponse.headers.get('etag');
expect(firstResponse.status).toBe(200);
expect(firstResponse.body).toBeDefined();
// Every request receives the etag
expect(etag).toBeDefined();
// Sending the etag in the if-none-match header should result in a 304
const secondRequest = new Request(`https://fake-worker.com/${targetId}/sdl`, {
headers: {
'x-hive-cdn-key': token,
'if-none-match': etag!,
},
});
const secondResponse = await handleRequest(secondRequest, KeyValidators.Bcrypt);
expect(secondResponse.status).toBe(304);
expect(secondResponse.body).toBeNull();
// Sending the etag in the if-none-match header should result in a 304
const wrongEtagRequest = new Request(`https://fake-worker.com/${targetId}/sdl`, {
headers: {
'x-hive-cdn-key': token,
'if-none-match': '"non-existing-etag"',
},
});
const wrongEtagResponse = await handleRequest(wrongEtagRequest, KeyValidators.Bcrypt);
expect(wrongEtagResponse.status).toBe(200);
expect(wrongEtagResponse.body).toBeDefined();
});
describe('Basic parsing errors', () => {
it('Should throw when target id is missing', async () => {
mockWorkerEnv({

View file

@ -169,3 +169,17 @@ Here's a list of available endpoints:
```
The value returned is the value you stored, as JSON, with `Content-Type: application/json` header.
## Notes
### ETag and If-None-Match headers
The CDN service accepts the [`ETag`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) and [`If-None-Match`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match) headers.
Every successful response from the CDN service (`200 OK`) contains the `ETag` header with a checksum.
If you send the same checksum in the `If-None-Match` header, the CDN service will return `304 Not Modified`, but only if the data hasn't changed.
If the data has changed, the CDN service will return `200 OK` with the new data and new `ETag` header.
Using `ETag` and `If-None-Mathc` helps to prevent unnecessary data transfer.
The `@graphql-hive/client` package uses this feature to save bandwidth and improve performance.