Skip to content

Commit

Permalink
Request/Response Compression Documentation + New Mesh Plugin to handl…
Browse files Browse the repository at this point in the history
…e upstream requests (#7428)

* Request/Response Compression Documentation + New Mesh Plugin to handle upstream requests

* Changeset

* Relax TS

* chore(dependencies): updated changesets for modified dependencies

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
ardatan and github-actions[bot] authored Jul 31, 2024
1 parent b92adf4 commit 6fc03b6
Show file tree
Hide file tree
Showing 11 changed files with 501 additions and 10 deletions.
5 changes: 5 additions & 0 deletions .changeset/@graphql-mesh_http-7428-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@graphql-mesh/http": patch
---
dependencies updates:
- Updated dependency [`@whatwg-node/server@^0.9.46` ↗︎](https://www.npmjs.com/package/@whatwg-node/server/v/0.9.46) (from `^0.9.34`, in `dependencies`)
5 changes: 5 additions & 0 deletions .changeset/@graphql-mesh_serve-runtime-7428-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@graphql-mesh/serve-runtime": patch
---
dependencies updates:
- Updated dependency [`@whatwg-node/server@^0.9.46` ↗︎](https://www.npmjs.com/package/@whatwg-node/server/v/0.9.46) (from `^0.9.34`, in `dependencies`)
20 changes: 20 additions & 0 deletions .changeset/chilled-melons-laugh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
'@graphql-mesh/serve-runtime': patch
---

New plugin to apply compression between subgraphs, gateway and the client
So Mesh can compress the request before sending it to the subgraph and decompress the response.
Then do the same for the response from the subgraph to the client.

```ts filename="mesh.config.ts"
import { defineConfig, useContentEncoding } from '@graphql-mesh/serve-cli'

export default defineConfig({
plugins: () => [
useContentEncoding({
subgraphs: ['*'] // Enable compression for all subgraphs
// subgraphs: ['subgraph1', 'subgraph2'] // Enable compression for specific subgraphs
})
]
})
```
4 changes: 2 additions & 2 deletions packages/fusion/composition/src/compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ import {
export interface SubgraphConfig {
name: string;
schema: GraphQLSchema;
isFederation?: boolean;
transforms?: SubgraphTransform[];
url?: string;
}

export type SubgraphTransform = (
Expand Down Expand Up @@ -64,7 +64,7 @@ export function getAnnotatedSubgraphs(
const annotatedSubgraphs: ServiceDefinition[] = [];
for (const subgraphConfig of subgraphs) {
const { name: subgraphName, schema, transforms } = subgraphConfig;
let url: string;
let url: string = subgraphConfig.url;
const subgraphSchemaExtensions = getDirectiveExtensions(schema);
const transportDirectives = subgraphSchemaExtensions?.transport;
if (transportDirectives?.length) {
Expand Down
2 changes: 1 addition & 1 deletion packages/legacy/http/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"tslib": "^2.4.0"
},
"dependencies": {
"@whatwg-node/server": "^0.9.34",
"@whatwg-node/server": "^0.9.46",
"graphql-yoga": "^5.6.0"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/serve-runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"@graphql-tools/utils": "^10.2.3",
"@graphql-tools/wrap": "^10.0.5",
"@whatwg-node/disposablestack": "^0.0.1",
"@whatwg-node/server": "^0.9.34",
"@whatwg-node/server": "^0.9.46",
"graphql-yoga": "^5.6.0"
},
"devDependencies": {
Expand Down
60 changes: 60 additions & 0 deletions packages/serve-runtime/src/useContentEncoding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { FetchAPI } from 'graphql-yoga';
import type { ExecutionRequest } from '@graphql-tools/utils';
import { useContentEncoding as useOrigContentEncoding } from '@whatwg-node/server';
import type { MeshServePlugin } from './types';

export interface UseContentEncodingOpts {
subgraphs?: string[];
}

export function useContentEncoding<TContext>({
subgraphs,
}: UseContentEncodingOpts): MeshServePlugin<TContext> {
if (!subgraphs?.length) {
return useOrigContentEncoding();
}
const compressionAlgorithm: CompressionFormat = 'gzip';
let fetchAPI: FetchAPI;
const execReqWithContentEncoding = new WeakSet<ExecutionRequest>();
return {
onYogaInit({ yoga }) {
fetchAPI = yoga.fetchAPI;
},
onPluginInit({ addPlugin }) {
addPlugin(
// @ts-expect-error - Typings are wrong
useOrigContentEncoding(),
);
},
onSubgraphExecute({ subgraphName, executionRequest }) {
if (subgraphs.includes(subgraphName)) {
execReqWithContentEncoding.add(executionRequest);
}
},
onFetch({ executionRequest, options, setOptions }) {
if (
options.body &&
!options.headers?.['Content-Encoding'] &&
execReqWithContentEncoding.has(executionRequest)
) {
const compressionStream = new fetchAPI.CompressionStream(compressionAlgorithm);
let bodyStream: ReadableStream;
if (options.body instanceof fetchAPI.ReadableStream) {
bodyStream = options.body;
} else {
// Create a fake Response and use its body to pipe through the compression stream
bodyStream = new fetchAPI.Response(options.body).body;
}
setOptions({
...options,
headers: {
'Accept-Encoding': 'gzip, deflate',
...options.headers,
'Content-Encoding': compressionAlgorithm,
},
body: bodyStream.pipeThrough(compressionStream),
});
}
},
};
}
160 changes: 160 additions & 0 deletions packages/serve-runtime/tests/useContentEncoding.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { createSchema, createYoga, type FetchAPI, type YogaInitialContext } from 'graphql-yoga';
import { getUnifiedGraphGracefully } from '@graphql-mesh/fusion-composition';
import { createServeRuntime } from '@graphql-mesh/serve-runtime';
import type { OnFetchHookDonePayload } from '@graphql-mesh/types';
import { useContentEncoding as useWhatwgNodeContentEncoding } from '@whatwg-node/server';
import { useContentEncoding } from '../src/useContentEncoding';

describe('useContentEncoding', () => {
const fooResolver = jest.fn((_, __, _context: YogaInitialContext) => {
return 'bar';
});
function decompressResponse(response: Response, fetchAPI: FetchAPI) {
const encodingFormat = response.headers.get('content-encoding');
const supportedFormats: CompressionFormat[] = ['gzip', 'deflate'];
if (!supportedFormats.includes(encodingFormat as CompressionFormat)) {
return response;
}
if (!response.body) {
return response;
}
const decompressionStream = new fetchAPI.DecompressionStream(
encodingFormat as CompressionFormat,
);
return new fetchAPI.Response(response.body.pipeThrough(decompressionStream), response);
}
// Mimic the behavior of the `fetch` API in the browser
const onFetchDoneSpy = jest.fn((payload: OnFetchHookDonePayload) => {
payload.setResponse(decompressResponse(payload.response, subgraphServer.fetchAPI));
});
const subgraphSchema = createSchema({
typeDefs: /* GraphQL */ `
type Query {
foo: String
}
`,
resolvers: {
Query: {
foo: fooResolver,
},
},
});
const subgraphServer = createYoga({
schema: subgraphSchema,
plugins: [useWhatwgNodeContentEncoding()],
});
const gateway = createServeRuntime({
supergraph() {
return getUnifiedGraphGracefully([
{
name: 'subgraph',
schema: subgraphSchema,
url: 'http://localhost:4001/graphql',
},
]);
},
fetchAPI: {
// @ts-expect-error - Typings are wrong
fetch: subgraphServer.fetch,
},
plugins: () => [
useContentEncoding({
subgraphs: ['subgraph'],
}),
{
onFetch() {
return onFetchDoneSpy;
},
},
],
});
afterEach(() => {
fooResolver.mockClear();
onFetchDoneSpy.mockClear();
});
it('from gateway to subgraph', async () => {
const response = await gateway.fetch('http://localhost:4000/graphql', {
method: 'POST',
body: JSON.stringify({
query: `query { foo }`,
}),
headers: {
'Content-Type': 'application/json',
},
});
const resJson = await response.json();
expect(resJson).toEqual({
data: {
foo: 'bar',
},
});
expect(fooResolver.mock.calls[0][2].request.headers.get('content-encoding')).toBe('gzip');
});
it('from subgraph to gateway', async () => {
const response = await gateway.fetch('http://localhost:4000/graphql', {
method: 'POST',
body: JSON.stringify({
query: `query { foo }`,
}),
headers: {
'Content-Type': 'application/json',
},
});
const resJson = await response.json();
expect(resJson).toEqual({
data: {
foo: 'bar',
},
});
expect(fooResolver.mock.calls[0][2].request.headers.get('accept-encoding')).toContain('gzip');
expect(onFetchDoneSpy.mock.calls[0][0].response.headers.get('content-encoding')).toBe('gzip');
});
it('from the client to the gateway', async () => {
const origBody = JSON.stringify({
query: `query { foo }`,
});
const fakeRequest = new gateway.fetchAPI.Request('http://localhost:4000/graphql', {
method: 'POST',
body: origBody,
headers: {
'Content-Type': 'application/json',
'Accept-Encoding': 'gzip',
},
});
const compressionStream = new gateway.fetchAPI.CompressionStream('gzip');
const response = await subgraphServer.fetch('http://localhost:4000/graphql', {
method: 'POST',
body: fakeRequest.body.pipeThrough(compressionStream),
headers: {
'Content-Type': 'application/json',
'Content-Encoding': 'gzip',
},
});
const resJson = await response.json();
expect(resJson).toEqual({
data: {
foo: 'bar',
},
});
});
it('from the gateway to the client', async () => {
const response = await gateway.fetch('http://localhost:4000/graphql', {
method: 'POST',
body: JSON.stringify({
query: `query { foo }`,
}),
headers: {
'Content-Type': 'application/json',
'Accept-Encoding': 'gzip',
},
});
expect(response.headers.get('content-encoding')).toBe('gzip');
const decompressedRes = decompressResponse(response, subgraphServer.fetchAPI);
const resJson = await decompressedRes.json();
expect(resJson).toEqual({
data: {
foo: 'bar',
},
});
});
});
1 change: 1 addition & 0 deletions website/src/pages/v1/serve/features/performance/_meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export default {
'response-caching': 'Response Caching',
'http-caching': 'Upstream HTTP Caching',
'deduplicate-request': 'Deduplicate HTTP Requests',
compression: 'Compression in HTTP',
'persisted-operations': 'Persisted Operations',
'automatic-persisted-queries': 'Automatic Persisted Queries',
'defer-stream': 'Defer & Stream',
Expand Down
Loading

0 comments on commit 6fc03b6

Please sign in to comment.