Skip to content

Commit 819c32f

Browse files
committed
Merge branch 'dev-v2.10.x' into 2.10.1
2 parents 9be7692 + 2871272 commit 819c32f

16 files changed

+565
-67
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
### Add `experimental_hoist_orphan_errors` configuration for controlling orphan error path assignment
2+
3+
Adds a new `experimental_hoist_orphan_errors` configuration that controls how entity-less ("orphan") errors from subgraphs are assigned paths in the response. When enabled for a subgraph, orphan errors are assigned to the nearest non-array ancestor in the response path, preventing them from being duplicated across every element in an array. This can be enabled globally via `all` or per-subgraph via the `subgraphs` map. Per-subgraph settings override `all`.
4+
5+
Here's an example when targeting a specific subgraph, `my_subgraph`:
6+
7+
```yaml
8+
experimental_hoist_orphan_errors:
9+
subgraphs:
10+
my_subgraph:
11+
enabled: true
12+
```
13+
14+
An example when targeting all subgraphs:
15+
16+
```yaml
17+
experimental_hoist_orphan_errors:
18+
all:
19+
enabled: true
20+
```
21+
22+
And an example enabling for all subgraphs except one:
23+
24+
```yaml
25+
experimental_hoist_orphan_errors:
26+
all:
27+
enabled: true
28+
subgraphs:
29+
noisy_one:
30+
enabled: false
31+
```
32+
33+
Using this feature should only happen if you know you have subgraphs that don't respond with the correct paths when making entity calls. If you're unsure, you probably don't need this!
34+
35+
36+
By [@aaronArinder](https://github.com/aaronArinder) in https://github.com/apollographql/router/pull/8998

apollo-router/src/configuration/mod.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,13 @@ pub struct Configuration {
222222
/// Type conditioned fetching configuration.
223223
#[serde(default)]
224224
pub(crate) experimental_type_conditioned_fetching: bool,
225+
226+
/// When enabled for specific subgraphs, orphan errors (those without a valid
227+
/// `_entities` path) are assigned to the nearest non-array ancestor in the
228+
/// response path, preventing them from being duplicated across every array
229+
/// element.
230+
#[serde(default)]
231+
pub(crate) experimental_hoist_orphan_errors: SubgraphConfiguration<HoistOrphanErrors>,
225232
}
226233

227234
impl PartialEq for Configuration {
@@ -256,6 +263,7 @@ impl<'de> serde::Deserialize<'de> for Configuration {
256263
experimental_chaos: chaos::Config,
257264
batching: Batching,
258265
experimental_type_conditioned_fetching: bool,
266+
experimental_hoist_orphan_errors: SubgraphConfiguration<HoistOrphanErrors>,
259267
}
260268
let mut ad_hoc: AdHocConfiguration = serde::Deserialize::deserialize(deserializer)?;
261269

@@ -287,6 +295,7 @@ impl<'de> serde::Deserialize<'de> for Configuration {
287295
limits: ad_hoc.limits,
288296
experimental_chaos: ad_hoc.experimental_chaos,
289297
experimental_type_conditioned_fetching: ad_hoc.experimental_type_conditioned_fetching,
298+
experimental_hoist_orphan_errors: ad_hoc.experimental_hoist_orphan_errors,
290299
plugins: ad_hoc.plugins,
291300
apollo_plugins: ad_hoc.apollo_plugins,
292301
batching: ad_hoc.batching,
@@ -327,6 +336,7 @@ impl Configuration {
327336
chaos: Option<chaos::Config>,
328337
uplink: Option<UplinkConfig>,
329338
experimental_type_conditioned_fetching: Option<bool>,
339+
experimental_hoist_orphan_errors: Option<SubgraphConfiguration<HoistOrphanErrors>>,
330340
batching: Option<Batching>,
331341
server: Option<Server>,
332342
) -> Result<Self, ConfigurationError> {
@@ -356,6 +366,7 @@ impl Configuration {
356366
batching: batching.unwrap_or_default(),
357367
experimental_type_conditioned_fetching: experimental_type_conditioned_fetching
358368
.unwrap_or_default(),
369+
experimental_hoist_orphan_errors: experimental_hoist_orphan_errors.unwrap_or_default(),
359370
notify,
360371
};
361372

@@ -500,6 +511,7 @@ impl Configuration {
500511
uplink,
501512
experimental_type_conditioned_fetching: experimental_type_conditioned_fetching
502513
.unwrap_or_default(),
514+
experimental_hoist_orphan_errors: Default::default(),
503515
batching: batching.unwrap_or_default(),
504516
raw_yaml: None,
505517
};
@@ -1574,3 +1586,17 @@ impl Batching {
15741586
}
15751587
}
15761588
}
1589+
1590+
/// Per-subgraph configuration for hoisting orphan errors.
1591+
///
1592+
/// "Orphan errors" are errors from entity fetches that lack a valid `_entities` path.
1593+
/// When hoisting is enabled, these errors are assigned to the nearest non-array
1594+
/// ancestor in the response path, preventing them from being duplicated across
1595+
/// every element in an array.
1596+
#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1597+
#[serde(deny_unknown_fields)]
1598+
pub(crate) struct HoistOrphanErrors {
1599+
/// Enable hoisting of orphan errors for this subgraph.
1600+
#[serde(default)]
1601+
pub(crate) enabled: bool,
1602+
}

apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5481,6 +5481,18 @@ expression: "&schema"
54815481
}
54825482
]
54835483
},
5484+
"HoistOrphanErrors": {
5485+
"additionalProperties": false,
5486+
"description": "Per-subgraph configuration for hoisting orphan errors.\n\n\"Orphan errors\" are errors from entity fetches that lack a valid `_entities` path.\nWhen hoisting is enabled, these errors are assigned to the nearest non-array\nancestor in the response path, preventing them from being duplicated across\nevery element in an array.",
5487+
"properties": {
5488+
"enabled": {
5489+
"default": false,
5490+
"description": "Enable hoisting of orphan errors for this subgraph.",
5491+
"type": "boolean"
5492+
}
5493+
},
5494+
"type": "object"
5495+
},
54845496
"Homepage": {
54855497
"additionalProperties": false,
54865498
"description": "Configuration options pertaining to the home page.",
@@ -9476,6 +9488,31 @@ expression: "&schema"
94769488
},
94779489
"type": "object"
94789490
},
9491+
"SubgraphHoistOrphanErrorsConfiguration": {
9492+
"description": "Configuration options pertaining to the subgraph server component.",
9493+
"properties": {
9494+
"all": {
9495+
"allOf": [
9496+
{
9497+
"$ref": "#/definitions/HoistOrphanErrors"
9498+
}
9499+
],
9500+
"default": {
9501+
"enabled": false
9502+
},
9503+
"description": "options applying to all subgraphs"
9504+
},
9505+
"subgraphs": {
9506+
"additionalProperties": {
9507+
"$ref": "#/definitions/HoistOrphanErrors"
9508+
},
9509+
"default": {},
9510+
"description": "per subgraph options",
9511+
"type": "object"
9512+
}
9513+
},
9514+
"type": "object"
9515+
},
94799516
"SubgraphInvalidationConfig": {
94809517
"additionalProperties": false,
94819518
"properties": {
@@ -12065,6 +12102,20 @@ expression: "&schema"
1206512102
"experimental_diagnostics": {
1206612103
"$ref": "#/definitions/Config5"
1206712104
},
12105+
"experimental_hoist_orphan_errors": {
12106+
"allOf": [
12107+
{
12108+
"$ref": "#/definitions/SubgraphHoistOrphanErrorsConfiguration"
12109+
}
12110+
],
12111+
"default": {
12112+
"all": {
12113+
"enabled": false
12114+
},
12115+
"subgraphs": {}
12116+
},
12117+
"description": "When enabled for specific subgraphs, orphan errors (those without a valid\n`_entities` path) are assigned to the nearest non-array ancestor in the\nresponse path, preventing them from being duplicated across every array\nelement."
12118+
},
1206812119
"experimental_type_conditioned_fetching": {
1206912120
"default": false,
1207012121
"description": "Type conditioned fetching configuration.",

apollo-router/src/configuration/tests.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1322,3 +1322,106 @@ fn it_prevents_enablement_of_both_subgraph_caching_plugins() {
13221322
serde_json::from_value(make_config(Some(true), Some(true)));
13231323
config_result.expect_err("both plugins configured");
13241324
}
1325+
1326+
#[rstest::rstest]
1327+
#[case::all_enabled("some_subgraph_name", true, &[], true)]
1328+
#[case::all_enabled_unknown("unknown", true, &[], true)]
1329+
#[case::subgraph_enabled("some_subgraph_name", false, &[("some_subgraph_name", true)], true)]
1330+
#[case::subgraph_disabled("disabled", false, &[("disabled", false)], false)]
1331+
#[case::subgraph_unknown_falls_back_to_all("unknown", false, &[("some_subgraph_name", true)], false)]
1332+
#[case::default_hoists_nothing("anything", false, &[], false)]
1333+
#[case::all_with_subgraph_override("overridden", true, &[("overridden", false)], false)]
1334+
fn hoist_orphan_errors_get(
1335+
#[case] query_subgraph: &str,
1336+
#[case] all_enabled: bool,
1337+
#[case] subgraph_entries: &[(&str, bool)],
1338+
#[case] expected: bool,
1339+
) {
1340+
let config = super::subgraph::SubgraphConfiguration {
1341+
all: super::HoistOrphanErrors {
1342+
enabled: all_enabled,
1343+
},
1344+
subgraphs: subgraph_entries
1345+
.iter()
1346+
.map(|(k, v)| (k.to_string(), super::HoistOrphanErrors { enabled: *v }))
1347+
.collect(),
1348+
};
1349+
assert_eq!(config.get(query_subgraph).enabled, expected);
1350+
}
1351+
1352+
#[test]
1353+
fn hoist_orphan_errors_deserializes_with_subgraphs() {
1354+
let config: Configuration = serde_json::from_value(json!({
1355+
"experimental_hoist_orphan_errors": {
1356+
"subgraphs": {
1357+
"some_subgraph_name": {
1358+
"enabled": true
1359+
},
1360+
"other": {
1361+
"enabled": false
1362+
}
1363+
}
1364+
}
1365+
}))
1366+
.expect("valid config");
1367+
assert!(
1368+
config
1369+
.experimental_hoist_orphan_errors
1370+
.get("some_subgraph_name")
1371+
.enabled
1372+
);
1373+
assert!(!config.experimental_hoist_orphan_errors.get("other").enabled);
1374+
assert!(
1375+
!config
1376+
.experimental_hoist_orphan_errors
1377+
.get("unknown")
1378+
.enabled
1379+
);
1380+
}
1381+
1382+
#[test]
1383+
fn hoist_orphan_errors_deserializes_with_all() {
1384+
let config: Configuration = serde_json::from_value(json!({
1385+
"experimental_hoist_orphan_errors": {
1386+
"all": {
1387+
"enabled": true
1388+
}
1389+
}
1390+
}))
1391+
.expect("valid config");
1392+
assert!(
1393+
config
1394+
.experimental_hoist_orphan_errors
1395+
.get("anything")
1396+
.enabled
1397+
);
1398+
}
1399+
1400+
#[test]
1401+
fn hoist_orphan_errors_all_with_subgraph_override() {
1402+
let config: Configuration = serde_json::from_value(json!({
1403+
"experimental_hoist_orphan_errors": {
1404+
"all": {
1405+
"enabled": true
1406+
},
1407+
"subgraphs": {
1408+
"noisy_one": {
1409+
"enabled": false
1410+
}
1411+
}
1412+
}
1413+
}))
1414+
.expect("valid config");
1415+
assert!(
1416+
config
1417+
.experimental_hoist_orphan_errors
1418+
.get("anything")
1419+
.enabled
1420+
);
1421+
assert!(
1422+
!config
1423+
.experimental_hoist_orphan_errors
1424+
.get("noisy_one")
1425+
.enabled
1426+
);
1427+
}

0 commit comments

Comments
 (0)