Skip to content

Commit 4cf75cb

Browse files
authored
Configure Subgraph Error handling (#997)
1 parent 0f70298 commit 4cf75cb

File tree

10 files changed

+103
-54
lines changed

10 files changed

+103
-54
lines changed

.changeset/quick-buses-obey.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@graphql-tools/executor-http': major
3+
'@graphql-mesh/transport-http-callback': minor
4+
'@graphql-hive/gateway-runtime': minor
5+
'@graphql-tools/federation': patch
6+
---
7+
8+
- **BREAKING**: HTTP Executor no longer takes `serviceName` as an option.
9+
- Both HTTP executor and `@graphql-mesh/transport-http-callback` no longer handle `DOWNSTREAM_SERVICE_ERROR` error code with `serviceName`.
10+
- Gateway runtime handles subgraph errors on its own with `DOWNSTREAM_SERVICE_ERROR` error code and `serviceName` as a property. This behavior can be configured with `subgraphErrors` option of the `createGatewayRuntime` function or CLI config.
11+
12+
```ts
13+
subgraphError: {
14+
errorCode: 'DOWNSTREAM_SERVICE_ERROR', // or `false` to remove this code completely
15+
subgraphNameProp: 'serviceName' // or `false` to remove this prop completely
16+
}
17+
```

packages/executors/http/src/index.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,6 @@ export type AsyncImportFn = (moduleName: string) => PromiseLike<any>;
5757
export type SyncImportFn = (moduleName: string) => any;
5858

5959
export interface HTTPExecutorOptions {
60-
/**
61-
* The name of the service
62-
*/
63-
serviceName?: string;
6460
/**
6561
* The endpoint to use when querying the upstream API
6662
* @default '/graphql'
@@ -163,7 +159,6 @@ export function buildHTTPExecutor(
163159
): DisposableExecutor<any, HTTPExecutorOptions> {
164160
const printFn = options?.print ?? defaultPrintFn;
165161
let disposeCtrl: AbortController | undefined;
166-
const serviceName = options?.serviceName;
167162
const baseExecutor = (
168163
request: ExecutionRequest<any, any, any, HTTPExecutorOptions>,
169164
excludeQuery?: boolean,
@@ -237,8 +232,6 @@ export function buildHTTPExecutor(
237232
const signal = abortSignalAny(signals);
238233

239234
const upstreamErrorExtensions: UpstreamErrorExtensions = {
240-
code: 'DOWNSTREAM_SERVICE_ERROR',
241-
serviceName,
242235
request: {
243236
method,
244237
},
@@ -442,15 +435,7 @@ export function buildHTTPExecutor(
442435
}: {
443436
message: string;
444437
extensions: Record<string, unknown>;
445-
}) =>
446-
createGraphQLError(message, {
447-
...options,
448-
extensions: {
449-
code: 'DOWNSTREAM_SERVICE_ERROR',
450-
serviceName,
451-
...(options.extensions || {}),
452-
},
453-
}),
438+
}) => createGraphQLError(message, options),
454439
),
455440
};
456441
}

packages/executors/http/tests/buildHTTPExecutor.test.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -285,14 +285,7 @@ describe('buildHTTPExecutor', () => {
285285
`),
286286
});
287287
expect(res).toMatchObject({
288-
errors: expect.arrayContaining([
289-
expect.any(GraphQLError),
290-
expect.objectContaining({
291-
extensions: {
292-
code: 'DOWNSTREAM_SERVICE_ERROR',
293-
},
294-
}),
295-
]),
288+
errors: expect.arrayContaining([expect.any(GraphQLError)]),
296289
});
297290
});
298291

packages/federation/src/supergraph.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1053,7 +1053,6 @@ export function getStitchingOptionsFromSupergraphSdl(
10531053
}
10541054
let executor: Executor = buildHTTPExecutor({
10551055
endpoint,
1056-
serviceName: subgraphName,
10571056
...httpExecutorOpts,
10581057
});
10591058
if (globalThis.process?.env?.['DEBUG']) {

packages/federation/tests/unavailable-subgraph.test.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -187,12 +187,10 @@ describe('Yoga gateway - subgraph unavailable', () => {
187187
{
188188
message: expect.stringContaining('connect'),
189189
extensions: {
190-
code: 'DOWNSTREAM_SERVICE_ERROR',
191190
request: {
192191
body: '{"query":"{__typename testNestedField{subgraph2{testSuccessQuery{id email sub2}}}}"}',
193192
method: 'POST',
194193
},
195-
serviceName: 'SUBGRAPH2',
196194
},
197195
path: ['testNestedField'],
198196
},
@@ -230,12 +228,10 @@ describe('Yoga gateway - subgraph unavailable', () => {
230228
{
231229
message: expect.stringContaining('connect'),
232230
extensions: {
233-
code: 'DOWNSTREAM_SERVICE_ERROR',
234231
request: {
235232
body: '{"query":"{__typename testNestedField{subgraph2{testErrorQuery{id email sub2}}}}"}',
236233
method: 'POST',
237234
},
238-
serviceName: 'SUBGRAPH2',
239235
},
240236
path: ['testNestedField'],
241237
},

packages/runtime/src/createGatewayRuntime.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ import useHiveConsole from './plugins/useHiveConsole';
9898
import { usePropagateHeaders } from './plugins/usePropagateHeaders';
9999
import { useRequestId } from './plugins/useRequestId';
100100
import { useRetryOnSchemaReload } from './plugins/useRetryOnSchemaReload';
101+
import { useSubgraphErrorPlugin } from './plugins/useSubgraphErrorPlugin';
101102
import { useSubgraphExecuteDebug } from './plugins/useSubgraphExecuteDebug';
102103
import { useUpstreamCancel } from './plugins/useUpstreamCancel';
103104
import { useUpstreamRetry } from './plugins/useUpstreamRetry';
@@ -991,6 +992,16 @@ export function createGatewayRuntime<
991992
useRetryOnSchemaReload({ logger }),
992993
];
993994

995+
if (config.subgraphErrors !== false) {
996+
basePlugins.push(
997+
useSubgraphErrorPlugin(
998+
typeof config.subgraphErrors === 'object'
999+
? config.subgraphErrors
1000+
: undefined,
1001+
),
1002+
);
1003+
}
1004+
9941005
if (config.requestId !== false) {
9951006
const reqIdPlugin = useRequestId(
9961007
typeof config.requestId === 'object' ? config.requestId : undefined,
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { isAsyncIterable } from '@graphql-tools/utils';
2+
import { GraphQLError } from 'graphql';
3+
import { GatewayPlugin } from '../types';
4+
5+
export interface SubgraphErrorPluginOptions {
6+
/**
7+
* The error code for the error that occurred in the subgraph.
8+
*
9+
* If set to `false`, the error code will not be included in the error.
10+
*
11+
* @default 'DOWNSTREAM_SERVICE_ERROR'
12+
*/
13+
errorCode?: string | false;
14+
15+
/**
16+
* The name of the extension field for the name of the subgraph
17+
*
18+
* If set to `false`, the subgraph name will not be included in the error.
19+
*
20+
* @default 'serviceName'
21+
*/
22+
subgraphNameProp?: string | false;
23+
}
24+
25+
export function useSubgraphErrorPlugin<
26+
TContext extends Record<string, unknown>,
27+
>({
28+
errorCode = 'DOWNSTREAM_SERVICE_ERROR',
29+
subgraphNameProp = 'serviceName',
30+
}: SubgraphErrorPluginOptions = {}): GatewayPlugin<TContext> {
31+
function extendError(error: GraphQLError, subgraphName: string) {
32+
// @ts-expect-error - we know "extensions" is a property of GraphQLError
33+
const errorExtensions = (error.extensions ||= {});
34+
if (errorCode) {
35+
errorExtensions.code ||= errorCode;
36+
}
37+
if (subgraphNameProp) {
38+
errorExtensions[subgraphNameProp] ||= subgraphName;
39+
}
40+
}
41+
return {
42+
onSubgraphExecute({ subgraphName }) {
43+
return function ({ result }) {
44+
if (isAsyncIterable(result)) {
45+
return {
46+
onNext({ result }) {
47+
if (result.errors) {
48+
for (const error of result.errors) {
49+
extendError(error, subgraphName);
50+
}
51+
}
52+
},
53+
};
54+
}
55+
if (result.errors) {
56+
for (const error of result.errors) {
57+
extendError(error, subgraphName);
58+
}
59+
}
60+
return;
61+
};
62+
},
63+
};
64+
}

packages/runtime/src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { DemandControlPluginOptions } from './plugins/useDemandControl';
4242
import { HiveConsolePluginOptions } from './plugins/useHiveConsole';
4343
import { PropagateHeadersOpts } from './plugins/usePropagateHeaders';
4444
import { RequestIdOptions } from './plugins/useRequestId';
45+
import { SubgraphErrorPluginOptions } from './plugins/useSubgraphErrorPlugin';
4546
import { UpstreamRetryPluginOptions } from './plugins/useUpstreamRetry';
4647
import { UpstreamTimeoutPluginOptions } from './plugins/useUpstreamTimeout';
4748

@@ -661,6 +662,11 @@ interface GatewayConfigBase<TContext extends Record<string, any>> {
661662
* @experimental
662663
*/
663664
__experimental__batchDelegation?: boolean;
665+
666+
/**
667+
* Subgraph error handling
668+
*/
669+
subgraphErrors?: SubgraphErrorPluginOptions | false;
664670
}
665671

666672
interface DisableIntrospectionOptions {

packages/transports/http-callback/src/index.ts

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -275,31 +275,10 @@ export default {
275275
break;
276276
case 'complete':
277277
if (message.errors) {
278-
if (message.errors.length === 1 && message.errors[0]) {
279-
const error = message.errors[0];
280-
stopSubscription(
281-
createGraphQLError(error.message, {
282-
...error,
283-
extensions: {
284-
...error.extensions,
285-
code: 'DOWNSTREAM_SERVICE_ERROR',
286-
},
287-
}),
288-
);
278+
if (message.errors.length === 1) {
279+
stopSubscription(message.errors[0]);
289280
} else {
290-
stopSubscription(
291-
new AggregateError(
292-
message.errors.map((err) =>
293-
createGraphQLError(err.message, {
294-
...err,
295-
extensions: {
296-
...err.extensions,
297-
code: 'DOWNSTREAM_SERVICE_ERROR',
298-
},
299-
}),
300-
),
301-
),
302-
);
281+
stopSubscription(new AggregateError(message.errors));
303282
}
304283
} else {
305284
stopSubscription();

packages/transports/http/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ export default {
5656
: undefined,
5757
...payload.transportEntry.options,
5858
getDisposeReason: payload.getDisposeReason,
59-
serviceName: payload.subgraphName,
6059
// @ts-expect-error - TODO: Fix this in executor-http
6160
fetch: payload.fetch,
6261
});

0 commit comments

Comments
 (0)