-
Notifications
You must be signed in to change notification settings - Fork 349
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Request/Response Compression Documentation + New Mesh Plugin to handl…
…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
1 parent
b92adf4
commit 6fc03b6
Showing
11 changed files
with
501 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}) | ||
] | ||
}) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
160
packages/serve-runtime/tests/useContentEncoding.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}, | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.