Skip to content

Commit c7ea2c5

Browse files
kroupaczTomas Kroupaardatan
authored
When the root field is shared between subgraphs and one of them throws, the errors are swallowed (#950)
Co-authored-by: Tomas Kroupa <tomas.kroupa@applifting.cz> Co-authored-by: Arda TANRIKULU <ardatanrikulu@gmail.com>
1 parent 9ffe414 commit c7ea2c5

File tree

10 files changed

+350
-69
lines changed

10 files changed

+350
-69
lines changed

.changeset/fast-beds-pump.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@graphql-tools/delegate': patch
3+
'@graphql-tools/federation': patch
4+
'@graphql-mesh/fusion-runtime': patch
5+
'@graphql-hive/gateway': patch
6+
'@graphql-tools/stitch': patch
7+
'@graphql-mesh/transport-common': patch
8+
---
9+
10+
Errors should not be swallowed when it is thrown from the shared root

.github/workflows/test.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ env:
1717
jobs:
1818
unit:
1919
strategy:
20+
fail-fast: false
2021
matrix:
2122
node-version:
2223
- 18
@@ -54,6 +55,7 @@ jobs:
5455

5556
leaks:
5657
strategy:
58+
fail-fast: false
5759
matrix:
5860
node-version:
5961
- 18

packages/delegate/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
},
4040
"dependencies": {
4141
"@graphql-tools/batch-execute": "workspace:^",
42-
"@graphql-tools/executor": "^1.3.10",
42+
"@graphql-tools/executor": "^1.4.7",
4343
"@graphql-tools/schema": "^10.0.11",
4444
"@graphql-tools/utils": "^10.8.1",
4545
"@repeaterjs/repeater": "^3.0.6",

packages/federation/src/supergraph.ts

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,6 +1052,7 @@ export function getStitchingOptionsFromSupergraphSdl(
10521052
}
10531053
let executor: Executor = buildHTTPExecutor({
10541054
endpoint,
1055+
serviceName: subgraphName,
10551056
...httpExecutorOpts,
10561057
});
10571058
if (globalThis.process?.env?.['DEBUG']) {
@@ -1307,6 +1308,31 @@ export function getStitchingOptionsFromSupergraphSdl(
13071308
}
13081309
const jobs: Promise<void>[] = [];
13091310
let hasPromise = false;
1311+
const fieldNames = new Set<string>();
1312+
currentAvailableSelectionSet?.selections.forEach((selection) => {
1313+
if (
1314+
selection.kind === Kind.FIELD &&
1315+
selection.name.value === info.fieldName
1316+
) {
1317+
selection.selectionSet?.selections.forEach((selection) => {
1318+
if (selection.kind === Kind.FIELD) {
1319+
fieldNames.add(selection.name.value);
1320+
}
1321+
});
1322+
}
1323+
});
1324+
currentUnavailableSelectionSet?.selections.forEach((selection) => {
1325+
if (
1326+
selection.kind === Kind.FIELD &&
1327+
selection.name.value === info.fieldName
1328+
) {
1329+
selection.selectionSet?.selections.forEach((selection) => {
1330+
if (selection.kind === Kind.FIELD) {
1331+
fieldNames.add(selection.name.value);
1332+
}
1333+
});
1334+
}
1335+
});
13101336
const mainJob = delegateToSchema({
13111337
schema: currentSubschema,
13121338
operation:
@@ -1357,9 +1383,11 @@ export function getStitchingOptionsFromSupergraphSdl(
13571383
return jobs[0];
13581384
}
13591385
if (hasPromise) {
1360-
return Promise.all(jobs).then((results) => mergeResults(results));
1386+
return Promise.all(jobs).then((results) =>
1387+
mergeResults(results, fieldNames),
1388+
);
13611389
}
1362-
return mergeResults(jobs);
1390+
return mergeResults(jobs, fieldNames);
13631391
};
13641392
if (operationType === 'subscription') {
13651393
return {
@@ -1573,14 +1601,50 @@ const specifiedTypeNames = [
15731601
'_Entity',
15741602
];
15751603

1576-
function makeExternalObject(data: any, errors: Error[]) {
1604+
function makeExternalObject(
1605+
data: any,
1606+
errors: Error[],
1607+
fieldNames: Set<string>,
1608+
) {
15771609
if (!isExternalObject(data) && typeof data === 'object' && data != null) {
15781610
data[UNPATHED_ERRORS_SYMBOL] = errors;
15791611
}
1612+
if (errors.length) {
1613+
const errorsToPop = [...errors];
1614+
const fieldNamesToPop: string[] = [];
1615+
for (const fieldName of fieldNames) {
1616+
if (data?.[fieldName] == null) {
1617+
fieldNamesToPop.push(fieldName);
1618+
}
1619+
}
1620+
while (fieldNamesToPop.length && errorsToPop.length) {
1621+
const fieldName = fieldNamesToPop.pop();
1622+
if (!fieldName) {
1623+
break;
1624+
}
1625+
const error = errorsToPop.pop();
1626+
if (!error) {
1627+
break;
1628+
}
1629+
let errorToSet: Error | undefined;
1630+
if (fieldNamesToPop.length || !errorsToPop.length) {
1631+
errorToSet = error;
1632+
} else {
1633+
errorToSet = new AggregateError(
1634+
errorsToPop,
1635+
errorsToPop.map((error) => error.message).join(', \n'),
1636+
);
1637+
}
1638+
if (errorToSet) {
1639+
data ||= {};
1640+
data[fieldName] = errorToSet;
1641+
}
1642+
}
1643+
}
15801644
return data;
15811645
}
15821646

1583-
function mergeResults(results: unknown[]) {
1647+
function mergeResults(results: unknown[], fieldNames: Set<string>) {
15841648
const errors: Error[] = [];
15851649
const datas: unknown[] = [];
15861650
for (const result of results) {
@@ -1594,9 +1658,13 @@ function mergeResults(results: unknown[]) {
15941658
}
15951659
if (datas.length) {
15961660
if (datas.length === 1) {
1597-
return makeExternalObject(datas[0], errors);
1661+
return makeExternalObject(datas[0], errors, fieldNames);
15981662
}
1599-
return makeExternalObject(mergeDeep(datas, undefined, true, true), errors);
1663+
return makeExternalObject(
1664+
mergeDeep(datas, undefined, true, true),
1665+
errors,
1666+
fieldNames,
1667+
);
16001668
}
16011669
if (errors.length) {
16021670
if (errors.length === 1) {

0 commit comments

Comments
 (0)