Skip to content

Commit 39784f7

Browse files
authored
feat(router): Subgraph endpoint overrides (#488)
This adds a way to override subgraph URLs at runtime - either statically or dynamically per request using expressions. ```yaml override_subgraph_urls: accounts: url: expression: | if .request.headers."x-region" == "eu" { "https://eu.example.com/accounts" } else { .original_url # represents the subgraph url defined in supergraph sdl } products: url: "https://example.com/products" ``` --- I also slightly changed the e2e testing setup as I was facing problems with race conditions. --- Fixes #461 Closes #471 Ref ROUTER-149
1 parent 9087fd8 commit 39784f7

File tree

21 files changed

+683
-108
lines changed

21 files changed

+683
-108
lines changed

.cargo/config.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
router = "run --package hive-router"
44
dev = "run --package qp-dev-cli"
55
subgraphs = "run --package subgraphs"
6-
test_all = "test --workspace"
76
test_qp = "test --package hive-router-query-planner -- --nocapture"
8-
test_e2e = "test --package e2e -- --nocapture"
7+
test_all = "test --workspace --exclude e2e"
8+
test_e2e = "test --package e2e --jobs 1 -- --nocapture --test-threads=1"
99
test_qpe = "test --package hive-router-plan-executor -- --nocapture"
1010
"clippy:fix" = "clippy --all --fix --allow-dirty --allow-staged"
1111
"router-config" = "run --release -p hive-router-config router-config.schema.json"

.github/workflows/ci.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ jobs:
2424
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
2525
- uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1
2626
- run: cargo test_all
27+
- run: cargo test_e2e
2728

2829
build-release:
2930
name: cargo build

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bench/subgraphs/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ pub struct RequestLog {
9595
#[derive(Clone)]
9696
pub struct SubgraphsServiceState {
9797
pub request_log: Arc<Mutex<HashMap<String, Vec<RequestLog>>>>,
98+
pub health_check_url: String,
9899
}
99100

100101
pub fn start_subgraphs_server(
@@ -108,6 +109,7 @@ pub fn start_subgraphs_server(
108109

109110
let shared_state = SubgraphsServiceState {
110111
request_log: Arc::new(Mutex::new(HashMap::new())),
112+
health_check_url: format!("http://{}:{}/health", host, port),
111113
};
112114

113115
let app = Router::new()

bin/router/src/jwt/errors.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,7 @@ impl From<&JwtError> for StatusCode {
8787
impl From<&JwtError> for GraphQLError {
8888
fn from(val: &JwtError) -> Self {
8989
GraphQLError {
90-
extensions: GraphQLErrorExtensions {
91-
code: Some(val.error_code().to_string()),
92-
..Default::default()
93-
},
90+
extensions: GraphQLErrorExtensions::new_from_code(val.error_code()),
9491
message: val.to_string(),
9592
locations: None,
9693
path: None,

bin/router/src/shared_state.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ pub struct RouterSharedState {
3131
pub validate_cache: Cache<u64, Arc<Vec<ValidationError>>>,
3232
pub parse_cache: Cache<u64, Arc<graphql_parser::query::Document<'static, String>>>,
3333
pub normalize_cache: Cache<u64, Arc<GraphQLNormalizationPayload>>,
34-
pub router_config: HiveRouterConfig,
34+
pub router_config: Arc<HiveRouterConfig>,
3535
pub headers_plan: HeaderRulesPlan,
3636
pub cors: Option<Cors>,
3737
pub jwt_auth_runtime: Option<JwtAuthRuntime>,
@@ -47,10 +47,11 @@ impl RouterSharedState {
4747
let planner =
4848
Planner::new_from_supergraph(&parsed_supergraph_sdl).expect("failed to create planner");
4949
let schema_metadata = planner.consumer_schema.schema_metadata();
50+
let router_config = Arc::new(router_config);
5051

5152
let subgraph_executor_map = SubgraphExecutorMap::from_http_endpoint_map(
5253
supergraph_state.subgraph_endpoint_map,
53-
router_config.traffic_shaping.clone(),
54+
router_config.clone(),
5455
)
5556
.expect("Failed to create subgraph executor map");
5657

docs/README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
|[**http**](#http)|`object`|Configuration for the HTTP server/listener.<br/>Default: `{"host":"0.0.0.0","port":4000}`<br/>||
1111
|[**jwt**](#jwt)|`object`, `null`|Configuration for JWT authentication plugin.<br/>|yes|
1212
|[**log**](#log)|`object`|The router logger configuration.<br/>Default: `{"filter":null,"format":"json","level":"info"}`<br/>||
13+
|[**override\_subgraph\_urls**](#override_subgraph_urls)|`object`|Configuration for overriding subgraph URLs.<br/>Default: `{}`<br/>||
1314
|[**query\_planner**](#query_planner)|`object`|Query planning configuration.<br/>Default: `{"allow_expose":false,"timeout":"10s"}`<br/>||
1415
|[**supergraph**](#supergraph)|`object`|Configuration for the Federation supergraph source. By default, the router will use a local file-based supergraph source (`./supergraph.graphql`).<br/>||
1516
|[**traffic\_shaping**](#traffic_shaping)|`object`|Configuration for the traffic-shaper executor. Use these configurations to control how requests are being executed to subgraphs.<br/>Default: `{"dedupe_enabled":true,"max_connections_per_host":100,"pool_idle_timeout_seconds":50}`<br/>||
@@ -59,6 +60,21 @@ log:
5960
filter: null
6061
format: json
6162
level: info
63+
override_subgraph_urls:
64+
accounts:
65+
url: https://accounts.example.com/graphql
66+
products:
67+
url:
68+
expression: |2-
69+
70+
if .request.headers."x-region" == "us-east" {
71+
"https://products-us-east.example.com/graphql"
72+
} else if .request.headers."x-region" == "eu-west" {
73+
"https://products-eu-west.example.com/graphql"
74+
} else {
75+
.original_url
76+
}
77+
6278
query_planner:
6379
allow_expose: false
6480
timeout: 10s
@@ -1497,6 +1513,47 @@ level: info
14971513
14981514
```
14991515

1516+
<a name="override_subgraph_urls"></a>
1517+
## override\_subgraph\_urls: object
1518+
1519+
Configuration for overriding subgraph URLs.
1520+
1521+
1522+
**Additional Properties**
1523+
1524+
|Name|Type|Description|Required|
1525+
|----|----|-----------|--------|
1526+
|[**Additional Properties**](#override_subgraph_urlsadditionalproperties)|`object`||yes|
1527+
1528+
**Example**
1529+
1530+
```yaml
1531+
accounts:
1532+
url: https://accounts.example.com/graphql
1533+
products:
1534+
url:
1535+
expression: |2-
1536+
1537+
if .request.headers."x-region" == "us-east" {
1538+
"https://products-us-east.example.com/graphql"
1539+
} else if .request.headers."x-region" == "eu-west" {
1540+
"https://products-eu-west.example.com/graphql"
1541+
} else {
1542+
.original_url
1543+
}
1544+
1545+
1546+
```
1547+
1548+
<a name="override_subgraph_urlsadditionalproperties"></a>
1549+
### override\_subgraph\_urls\.additionalProperties: object
1550+
1551+
**Properties**
1552+
1553+
|Name|Type|Description|Required|
1554+
|----|----|-----------|--------|
1555+
|**url**||Overrides for the URL of the subgraph.<br/><br/>For convenience, a plain string in your configuration will be treated as a static URL.<br/><br/>### Static URL Example<br/>```yaml<br/>url: "https://api.example.com/graphql"<br/>```<br/><br/>### Dynamic Expression Example<br/><br/>The expression has access to the following variables:<br/>- `request`: The incoming HTTP request, including headers and other metadata.<br/>- `original_url`: The original URL of the subgraph (from supergraph sdl).<br/><br/>```yaml<br/>url:<br/> expression: \|<br/> if .request.headers."x-region" == "us-east" {<br/> "https://products-us-east.example.com/graphql"<br/> } else if .request.headers."x-region" == "eu-west" {<br/> "https://products-eu-west.example.com/graphql"<br/> } else {<br/> .original_url<br/> }<br/>|yes|
1556+
15001557
<a name="query_planner"></a>
15011558
## query\_planner: object
15021559

e2e/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ lazy_static = { workspace = true }
1717
jsonwebtoken = { workspace = true }
1818
insta = { workspace = true }
1919

20+
reqwest = "0.12.23"
21+
2022
hive-router = { path = "../bin/router" }
2123
hive-router-config = { path = "../lib/router-config" }
2224
subgraphs = { path = "../bench/subgraphs" }
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# yaml-language-server: $schema=../../../router-config.schema.json
2+
supergraph:
3+
source: file
4+
path: ../../supergraph.graphql
5+
override_subgraph_urls:
6+
accounts:
7+
url:
8+
expression: |
9+
if .request.headers."x-accounts-port" == "4100" {
10+
"http://0.0.0.0:4100/accounts"
11+
} else {
12+
.original_url
13+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# yaml-language-server: $schema=../../../router-config.schema.json
2+
supergraph:
3+
source: file
4+
path: ../../supergraph.graphql
5+
override_subgraph_urls:
6+
accounts:
7+
url: "http://0.0.0.0:4100/accounts"

0 commit comments

Comments
 (0)