From ae6c58f4404f586a04245670418a1104d90e04cc Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Fri, 19 Sep 2025 12:13:33 +0200 Subject: [PATCH 1/2] Add request header propagation and modification Initial code for response headers Add subgraph header middleware Refactor response header handling Fix header error messages and improve clarity descriptions no default algo Adds expressions to headers propagation (#453) tests snaps slight adjustments to config to make it simpler --- Cargo.lock | 2406 +++++++++++++++++++++- bench/subgraphs/main.rs | 16 + bin/router/src/lib.rs | 2 +- bin/router/src/pipeline/execution.rs | 29 +- bin/router/src/pipeline/mod.rs | 21 +- bin/router/src/shared_state.rs | 17 +- docs/README.md | 1191 ++++++++++- lib/executor/Cargo.toml | 4 + lib/executor/src/context.rs | 8 +- lib/executor/src/execution/error.rs | 12 +- lib/executor/src/execution/plan.rs | 225 +- lib/executor/src/executors/common.rs | 12 +- lib/executor/src/executors/dedupe.rs | 75 +- lib/executor/src/executors/http.rs | 71 +- lib/executor/src/executors/map.rs | 21 +- lib/executor/src/headers/compile.rs | 484 +++++ lib/executor/src/headers/errors.rs | 47 + lib/executor/src/headers/expression.rs | 147 ++ lib/executor/src/headers/mod.rs | 621 ++++++ lib/executor/src/headers/plan.rs | 132 ++ lib/executor/src/headers/request.rs | 243 +++ lib/executor/src/headers/response.rs | 353 ++++ lib/executor/src/headers/sanitizer.rs | 11 + lib/executor/src/lib.rs | 1 + lib/router-config/Cargo.toml | 1 + lib/router-config/src/headers.rs | 384 ++++ lib/router-config/src/lib.rs | 14 +- lib/router-config/src/traffic_shaping.rs | 11 - 28 files changed, 6287 insertions(+), 272 deletions(-) create mode 100644 lib/executor/src/headers/compile.rs create mode 100644 lib/executor/src/headers/errors.rs create mode 100644 lib/executor/src/headers/expression.rs create mode 100644 lib/executor/src/headers/mod.rs create mode 100644 lib/executor/src/headers/plan.rs create mode 100644 lib/executor/src/headers/request.rs create mode 100644 lib/executor/src/headers/response.rs create mode 100644 lib/executor/src/headers/sanitizer.rs create mode 100644 lib/router-config/src/headers.rs diff --git a/Cargo.lock b/Cargo.lock index d5f116d79..844645fc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,6 +27,43 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-siv" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e08d0cdb774acd1e4dac11478b1a0c0d203134b2aab0ba25eb430de9b18f8b9" +dependencies = [ + "aead", + "aes", + "cipher", + "cmac", + "ctr", + "dbl", + "digest", + "zeroize", +] + [[package]] name = "ahash" version = "0.8.12" @@ -36,6 +73,7 @@ dependencies = [ "cfg-if", "getrandom 0.3.3", "once_cell", + "serde", "version_check", "zerocopy", ] @@ -49,6 +87,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -64,18 +108,83 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + [[package]] name = "arraydeque" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "ascii-canvas" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1e3e699d84ab1b0911a1010c5c106aa34ae89aeac103be5ce0c3859db1e891" +dependencies = [ + "term", +] + [[package]] name = "ascii_utils" version = "0.9.3" @@ -308,9 +417,21 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-link", + "windows-link 0.2.0", ] +[[package]] +name = "base16" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27c3610c36aee21ce8ac510e6224498de4228ad772a171ed65643a24693a5a8" + +[[package]] +name = "base62" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1adf9755786e27479693dedd3271691a92b5e242ab139cacb9fb8e7fb5381111" + [[package]] name = "base64" version = "0.21.7" @@ -323,6 +444,37 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.9.4" @@ -341,12 +493,33 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "borrow-or-share" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eeab4423108c5d7c744f4d234de88d18d636100093ae04caf4825134b9c3a32" + [[package]] name = "bumpalo" version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + [[package]] name = "bytes" version = "1.10.1" @@ -362,6 +535,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.2.40" @@ -369,9 +551,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] +[[package]] +name = "cfb-mode" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "738b8d467867f80a71351933f70461f5b56f24d5c93e0cf216e59229c968d330" +dependencies = [ + "cipher", +] + [[package]] name = "cfg-if" version = "1.0.3" @@ -384,6 +577,40 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "charset" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f927b07c74ba84c7e5fe4db2baeb3e996ab2688992e39ac68ce3220a677c7e" +dependencies = [ + "base64 0.22.1", + "encoding_rs", +] + [[package]] name = "chrono" version = "0.4.42" @@ -391,9 +618,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", "serde", - "windows-link", + "wasm-bindgen", + "windows-link 0.2.0", +] + +[[package]] +name = "chrono-tz" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" +dependencies = [ + "chrono", + "phf", ] [[package]] @@ -423,6 +662,23 @@ dependencies = [ "half", ] +[[package]] +name = "cidr" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd1b64030216239a2e7c364b13cd96a2097ebf0dfe5025f2dedee14a23f2ab60" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + [[package]] name = "clap" version = "4.5.48" @@ -430,6 +686,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -438,8 +695,22 @@ version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" dependencies = [ + "anstream", "anstyle", "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] @@ -448,6 +719,34 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +[[package]] +name = "cmac" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8543454e3c3f5126effff9cd44d562af4e31fb8ce1cc0d3dcd8f084515dbc1aa" +dependencies = [ + "cipher", + "dbl", + "digest", +] + +[[package]] +name = "codespan-reporting" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "combine" version = "4.6.7" @@ -458,6 +757,17 @@ dependencies = [ "memchr", ] +[[package]] +name = "community-id" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48629740a3480b865d4083ff45f826a253bd5ce28db618d89359b0e95dc750c3" +dependencies = [ + "base64 0.22.1", + "hex", + "sha1", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -474,7 +784,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180e549344080374f9b32ed41bf3b6b57885ff6a289367b3dbc10eea8acc1918" dependencies = [ "async-trait", - "convert_case", + "convert_case 0.6.0", "json5", "pathdiff", "ron", @@ -538,6 +848,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "convert_case" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -548,6 +867,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -574,6 +903,30 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "criterion" version = "0.7.0" @@ -585,7 +938,7 @@ dependencies = [ "ciborium", "clap", "criterion-plot", - "itertools", + "itertools 0.13.0", "num-traits", "oorandom", "plotters", @@ -605,7 +958,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338" dependencies = [ "cast", - "itertools", + "itertools 0.13.0", ] [[package]] @@ -655,9 +1008,55 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "crypto_secretbox" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d6cf87adf719ddf43a805e92c6870a531aedda35ff640442cbaf8674e141e1" +dependencies = [ + "aead", + "cipher", + "generic-array", + "poly1305", + "salsa20", + "subtle", + "zeroize", +] + +[[package]] +name = "csv" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" +dependencies = [ + "memchr", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "ctrlc" version = "3.5.0" @@ -750,7 +1149,7 @@ dependencies = [ "hashbrown 0.14.5", "lock_api", "once_cell", - "parking_lot_core", + "parking_lot_core 0.9.12", ] [[package]] @@ -759,6 +1158,15 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +[[package]] +name = "dbl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd2735a791158376708f9347fe8faba9667589d82427ef3aed6794a8981de3d9" +dependencies = [ + "generic-array", +] + [[package]] name = "deranged" version = "0.5.4" @@ -770,13 +1178,25 @@ dependencies = [ ] [[package]] -name = "digest" +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -785,6 +1205,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "dlv-list" version = "0.5.2" @@ -794,6 +1225,57 @@ dependencies = [ "const-random", ] +[[package]] +name = "dns-lookup" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf5597a4b7fe5275fc9dcf88ce26326bc8e4cb87d0130f33752d4c5f717793cf" +dependencies = [ + "cfg-if", + "libc", + "socket2 0.6.0", + "windows-sys 0.60.2", +] + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "domain" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a11dd7f04a6a6d2aea0153c6e31f5ea7af8b2efdf52cdaeea7a9a592c7fefef9" +dependencies = [ + "bumpalo", + "bytes", + "domain-macros", + "futures-util", + "hashbrown 0.14.5", + "log", + "moka", + "octseq", + "rand 0.8.5", + "serde", + "smallvec", + "time", + "tokio", + "tracing", +] + +[[package]] +name = "domain-macros" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e197fdfd2cdb5fdeb7f8ddcf3aed5d5d04ecde2890d448b14ffb716f7376b70" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "dyn-clone" version = "1.0.20" @@ -806,6 +1288,24 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] + +[[package]] +name = "ena" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" +dependencies = [ + "log", +] + [[package]] name = "encode_unicode" version = "1.0.0" @@ -888,6 +1388,29 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fancy-regex" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6215aee357f8c7c989ebb4b8466ca4d7dc93b3957039f2fc3ea2ade8ea5f279" +dependencies = [ + "bit-set", + "derivative", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "fancy-regex" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + [[package]] name = "fast_chemail" version = "0.9.6" @@ -927,6 +1450,28 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" +[[package]] +name = "flate2" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" +dependencies = [ + "crc32fast", + "libz-rs-sys", + "miniz_oxide", +] + +[[package]] +name = "fluent-uri" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5" +dependencies = [ + "borrow-or-share", + "ref-cast", + "serde", +] + [[package]] name = "fnv" version = "1.0.7" @@ -963,6 +1508,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fraction" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f158e3ff0a1b334408dc9fb811cd99b446986f4d8b741bb08f9df1604085ae7" +dependencies = [ + "lazy_static", + "num", +] + [[package]] name = "futures" version = "0.3.31" @@ -1066,6 +1621,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1075,8 +1631,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1086,9 +1644,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasi 0.14.7+wasi-0.2.4", + "wasm-bindgen", ] [[package]] @@ -1097,6 +1657,12 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "graphql-parser" version = "0.4.1" @@ -1120,6 +1686,16 @@ dependencies = [ "serde_with", ] +[[package]] +name = "grok" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e2d7bd791814b06a609b74361ac35b448eb4718548937c6de718554a4348577" +dependencies = [ + "glob", + "onig", +] + [[package]] name = "h2" version = "0.4.12" @@ -1174,6 +1750,9 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "allocator-api2", +] [[package]] name = "hashbrown" @@ -1199,6 +1778,12 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -1235,7 +1820,7 @@ dependencies = [ "mimalloc", "moka", "ntex", - "rand", + "rand 0.9.2", "serde", "sonic-rs", "thiserror 2.0.17", @@ -1252,6 +1837,7 @@ name = "hive-router-config" version = "0.0.5" dependencies = [ "config", + "http", "humantime-serde", "schemars 1.0.4", "serde", @@ -1280,7 +1866,10 @@ dependencies = [ "hyper-tls", "hyper-util", "indexmap 2.11.4", + "insta", "itoa", + "ntex-http", + "regex-automata", "ryu", "serde", "sonic-rs", @@ -1288,6 +1877,7 @@ dependencies = [ "thiserror 2.0.17", "tokio", "tracing", + "vrl", "xxhash-rust", ] @@ -1295,14 +1885,14 @@ dependencies = [ name = "hive-router-query-planner" version = "2.0.0" dependencies = [ - "bitflags", + "bitflags 2.9.4", "criterion", "graphql-parser", "graphql-tools", "insta", "lazy-init", "lazy_static", - "petgraph", + "petgraph 0.8.3", "rustc-hash", "serde", "sonic-rs", @@ -1313,6 +1903,26 @@ dependencies = [ "tracing-tree", ] +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "hostname" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" +dependencies = [ + "cfg-if", + "libc", + "windows-link 0.1.3", +] + [[package]] name = "http" version = "1.3.1" @@ -1398,6 +2008,23 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.6.0" @@ -1420,6 +2047,7 @@ version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -1427,7 +2055,9 @@ dependencies = [ "http", "http-body", "hyper", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2 0.6.0", "tokio", @@ -1459,12 +2089,119 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -1488,6 +2225,35 @@ dependencies = [ "serde_core", ] +[[package]] +name = "indoc" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" + +[[package]] +name = "influxdb-line-protocol" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22fa7ee6be451ea0b1912b962c91c8380835e97cf1584a77e18264e908448dcb" +dependencies = [ + "bytes", + "log", + "nom 7.1.3", + "smallvec", + "snafu 0.7.5", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + [[package]] name = "insta" version = "1.43.2" @@ -1499,52 +2265,180 @@ dependencies = [ "similar", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "io-uring" version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ - "bitflags", + "bitflags 2.9.4", "cfg-if", "libc", ] [[package]] -name = "itertools" -version = "0.13.0" +name = "ipcrypt-rs" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +checksum = "96e4f67dbfc0f75d7b65953ecf0be3fd84ee0cb1ae72a00a4aa9a2f5518a2c80" dependencies = [ - "either", + "aes", ] [[package]] -name = "itoa" -version = "1.0.15" +name = "ipnet" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] -name = "js-sys" -version = "0.3.81" +name = "iri-string" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" dependencies = [ - "once_cell", - "wasm-bindgen", + "memchr", + "serde", ] [[package]] -name = "json5" -version = "0.4.1" +name = "is_terminal_polyfill" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" -dependencies = [ - "pest", - "pest_derive", - "serde", -] +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "jsonschema" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24690c68dfcdde5980d676b0f1820981841016b1f29eecb4c42ad48ab4118681" +dependencies = [ + "ahash", + "base64 0.22.1", + "bytecount", + "email_address", + "fancy-regex 0.16.2", + "fraction", + "idna", + "itoa", + "num-cmp", + "num-traits", + "once_cell", + "percent-encoding", + "referencing", + "regex", + "regex-syntax", + "serde", + "serde_json", + "uuid-simd", +] + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lalrpop" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4ebbd48ce411c1d10fb35185f5a51a7bfa3d8b24b4e330d30c9e3a34129501" +dependencies = [ + "ascii-canvas", + "bit-set", + "ena", + "itertools 0.14.0", + "lalrpop-util", + "petgraph 0.7.1", + "regex", + "regex-syntax", + "sha3", + "string_cache", + "term", + "unicode-xid", + "walkdir", +] + +[[package]] +name = "lalrpop-util" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5baa5e9ff84f1aefd264e6869907646538a52147a755d494517a8007fb48733" +dependencies = [ + "regex-automata", + "rustversion", +] [[package]] name = "lazy-init" @@ -1574,12 +2468,27 @@ dependencies = [ "libc", ] +[[package]] +name = "libz-rs-sys" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" +dependencies = [ + "zlib-rs", +] + [[package]] name = "linux-raw-sys" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + [[package]] name = "lock_api" version = "0.4.14" @@ -1595,6 +2504,21 @@ version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "lz4_flex" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" +dependencies = [ + "twox-hash", +] + [[package]] name = "matchers" version = "0.2.0" @@ -1610,6 +2534,16 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.7.6" @@ -1641,6 +2575,12 @@ dependencies = [ "walkdir", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1648,6 +2588,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -1674,7 +2615,7 @@ dependencies = [ "equivalent", "event-listener", "futures-util", - "parking_lot", + "parking_lot 0.12.5", "portable-atomic", "rustc_version", "smallvec", @@ -1737,23 +2678,63 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nix" version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags", + "bitflags 2.9.4", "cfg-if", "cfg_aliases", "libc", ] +[[package]] +name = "nohash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0f889fb66f7acdf83442c35775764b51fed3c606ab9cee51500dbde2cf528ca" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "nom-language" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2de2bc5b451bfedaef92c90b8939a8fff5770bdcc1fafd6239d086aab8fa6b29" +dependencies = [ + "nom 8.0.0", +] + [[package]] name = "ntex" version = "2.16.0" @@ -1761,7 +2742,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4b41d1af2e11a7c29499395c2505550507749c35b29fa4a31234130abdb2285" dependencies = [ "base64 0.22.1", - "bitflags", + "bitflags 2.9.4", "encoding_rs", "env_logger", "httparse", @@ -1799,7 +2780,7 @@ version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d23b86ef2f4a947e29e959a61bdae71c9d52a80df02936a9992bc6dbda9ddb" dependencies = [ - "bitflags", + "bitflags 2.9.4", "bytes", "futures-core", "serde", @@ -1821,7 +2802,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bfa6c16696b2b08cef057d581b67726213b71b42be69cda1977e351b9a36e5c" dependencies = [ "ahash", - "bitflags", + "bitflags 2.9.4", "log", "nanorand", "ntex-bytes", @@ -1857,7 +2838,7 @@ version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55eb13ef2e89f799ef0395911b6365052cab4cea65a7d2ef870e39732bf346b2" dependencies = [ - "bitflags", + "bitflags 2.9.4", "log", "ntex-bytes", "ntex-codec", @@ -1883,7 +2864,7 @@ version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a53c65509e4e3abf6490514e98a570d4cbcdbf18628e9569a02cc1d47c1e29b9" dependencies = [ - "bitflags", + "bitflags 2.9.4", "cfg-if", "libc", "log", @@ -1989,7 +2970,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7811bcf3c3228631b0b20d12e5786c20c4cc76fb2d2b2733a6ab421641f81b6a" dependencies = [ "ahash", - "bitflags", + "bitflags 2.9.4", "futures-core", "futures-timer", "log", @@ -2010,12 +2991,82 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2044,31 +3095,85 @@ dependencies = [ "memchr", ] +[[package]] +name = "octseq" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126c3ca37c9c44cec575247f43a3e4374d8927684f129d2beeb0d2cef262fe12" +dependencies = [ + "bytes", + "serde", + "smallvec", +] + +[[package]] +name = "ofb" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc40678e045ff4eb1666ea6c0f994b133c31f673c09aed292261b6d5b6963a0" +dependencies = [ + "cipher", +] + [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + [[package]] name = "oneshot" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ce411919553d3f9fa53a0880544cda985a112117a0444d5ff1e870a893d6ea" +[[package]] +name = "onig" +version = "6.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" +dependencies = [ + "bitflags 2.9.4", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "oorandom" version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl" version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags", + "bitflags 2.9.4", "cfg-if", "foreign-types", "libc", @@ -2116,6 +3221,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-multimap" version = "0.7.3" @@ -2126,12 +3240,29 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + [[package]] name = "parking" version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -2139,7 +3270,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", - "parking_lot_core", + "parking_lot_core 0.9.12", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", ] [[package]] @@ -2150,11 +3295,17 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", - "windows-link", + "windows-link 0.2.0", ] +[[package]] +name = "parse-size" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b" + [[package]] name = "pathdiff" version = "0.2.3" @@ -2162,8 +3313,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] -name = "percent-encoding" -version = "2.3.2" +name = "peeking_take_while" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9ed2178b0575fff8e1b83b58ba6f75e727aafac2e1b6c795169ad3b17eb518" + +[[package]] +name = "percent-encoding" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" @@ -2210,6 +3367,16 @@ dependencies = [ "sha2", ] +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap 2.11.4", +] + [[package]] name = "petgraph" version = "0.8.3" @@ -2222,6 +3389,33 @@ dependencies = [ "serde", ] +[[package]] +name = "phf" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" +dependencies = [ + "phf_shared 0.12.1", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -2282,12 +3476,32 @@ dependencies = [ "windows-sys 0.61.1", ] +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -2303,6 +3517,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -2321,6 +3541,64 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "prost-reflect" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5edd582b62f5cde844716e66d92565d7faf7ab1445c8cebce6e00fba83ddb2" +dependencies = [ + "once_cell", + "prost", + "prost-types", +] + +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost", +] + +[[package]] +name = "psl" +version = "2.1.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b0efc2f09945ea5ef176dc4f1cb703b0efa41b112d5eb27489afc880db860a7" +dependencies = [ + "psl-types", +] + +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + [[package]] name = "ptr_meta" version = "0.3.1" @@ -2341,6 +3619,16 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + [[package]] name = "qp-dev-cli" version = "0.0.0" @@ -2363,6 +3651,61 @@ dependencies = [ "web-sys", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2 0.6.0", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.0", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.41" @@ -2372,6 +3715,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quoted_printable" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" + [[package]] name = "r-efi" version = "5.3.0" @@ -2387,14 +3736,35 @@ dependencies = [ "ptr_meta", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "rand_chacha", - "rand_core", + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", ] [[package]] @@ -2404,7 +3774,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", ] [[package]] @@ -2436,13 +3815,22 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.9.4", ] [[package]] @@ -2465,6 +3853,20 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "referencing" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a3d769362109497b240e66462606bc28af68116436c8669bac17069533b908e" +dependencies = [ + "ahash", + "fluent-uri", + "once_cell", + "parking_lot 0.12.5", + "percent-encoding", + "serde_json", +] + [[package]] name = "regex" version = "1.11.3" @@ -2488,6 +3890,20 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-filtered" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c11639076bf147be211b90e47790db89f4c22b6c8a9ca6e960833869da67166" +dependencies = [ + "aho-corasick", + "indexmap 2.11.4", + "itertools 0.13.0", + "nohash", + "regex", + "regex-syntax", +] + [[package]] name = "regex-syntax" version = "0.8.6" @@ -2500,6 +3916,104 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6" +[[package]] +name = "reqwest" +version = "0.12.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "reqwest-middleware" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e" +dependencies = [ + "anyhow", + "async-trait", + "http", + "reqwest", + "serde", + "thiserror 1.0.69", + "tower-service", +] + +[[package]] +name = "reqwest-retry" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c73e4195a6bfbcb174b790d9b3407ab90646976c55de58a6515da25d851178" +dependencies = [ + "anyhow", + "async-trait", + "futures", + "getrandom 0.2.16", + "http", + "hyper", + "parking_lot 0.11.2", + "reqwest", + "reqwest-middleware", + "retry-policies", + "thiserror 1.0.69", + "tokio", + "wasm-timer", +] + +[[package]] +name = "retry-policies" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5875471e6cab2871bc150ecb8c727db5113c9338cc3354dc5ee3425b6aa40a1c" +dependencies = [ + "rand 0.8.5", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rkyv" version = "0.8.12" @@ -2536,11 +4050,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ "base64 0.21.7", - "bitflags", + "bitflags 2.9.4", "serde", "serde_derive", ] +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + [[package]] name = "rust-ini" version = "0.21.3" @@ -2551,6 +4071,16 @@ dependencies = [ "ordered-multimap", ] +[[package]] +name = "rust_decimal" +version = "1.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8975fc98059f365204d635119cf9c5a60ae67b841ed49b5422a9a7e56cdfac0" +dependencies = [ + "arrayvec", + "num-traits", +] + [[package]] name = "rustc-demangle" version = "0.1.26" @@ -2578,13 +4108,60 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags", + "bitflags 2.9.4", "errno", "libc", "linux-raw-sys", "windows-sys 0.61.1", ] +[[package]] +name = "rustls" +version = "0.23.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.5.1", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -2597,6 +4174,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "same-file" version = "1.0.6" @@ -2658,14 +4244,33 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "security-framework" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags", - "core-foundation", + "bitflags 2.9.4", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags 2.9.4", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -2816,6 +4421,30 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.11.4", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha-1" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2838,6 +4467,16 @@ dependencies = [ "digest", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -2872,6 +4511,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "simdutf8" version = "0.1.5" @@ -2884,6 +4529,12 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.11" @@ -2896,6 +4547,55 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "snafu" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6" +dependencies = [ + "doc-comment", + "snafu-derive 0.7.5", +] + +[[package]] +name = "snafu" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2" +dependencies = [ + "snafu-derive 0.8.9", +] + +[[package]] +name = "snafu-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "snafu-derive" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "snap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" + [[package]] name = "socket2" version = "0.5.10" @@ -2961,12 +4661,39 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions_next" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7beae5182595e9a8b683fa98c4317f956c9a2dec3b9716990d20023cc60c766" +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot 0.12.5", + "phf_shared 0.11.3", + "precomputed-hash", +] + +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" +dependencies = [ + "vte", +] + [[package]] name = "strsim" version = "0.11.1" @@ -2988,7 +4715,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "rustversion", @@ -3003,10 +4730,16 @@ dependencies = [ "async-graphql-axum", "axum", "lazy_static", - "rand", + "rand 0.9.2", "tokio", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "1.0.109" @@ -3034,6 +4767,30 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "syslog_loose" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6ec4df26907adce53e94eac201a9ba38744baea3bc97f34ffd591d5646231a6" +dependencies = [ + "chrono", + "nom 8.0.0", +] [[package]] name = "tagptr" @@ -3054,6 +4811,24 @@ dependencies = [ "windows-sys 0.61.1", ] +[[package]] +name = "term" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2111ef44dae28680ae9752bb89409e7310ca33a8c621ebe7b106cf5c928b3ac0" +dependencies = [ + "windows-sys 0.61.1", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -3143,6 +4918,16 @@ dependencies = [ "crunchy", ] +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -3179,7 +4964,7 @@ dependencies = [ "io-uring", "libc", "mio", - "parking_lot", + "parking_lot 0.12.5", "pin-project-lite", "signal-hook-registry", "slab", @@ -3209,6 +4994,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.17" @@ -3293,16 +5088,34 @@ dependencies = [ name = "tower" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "futures-core", + "bitflags 2.9.4", + "bytes", "futures-util", + "http", + "http-body", + "iri-string", "pin-project-lite", - "sync_wrapper", - "tokio", + "tower", "tower-layer", "tower-service", - "tracing", ] [[package]] @@ -3422,12 +5235,18 @@ dependencies = [ "http", "httparse", "log", - "rand", + "rand 0.9.2", "sha1", "thiserror 2.0.17", "utf-8", ] +[[package]] +name = "twox-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" + [[package]] name = "typeid" version = "1.0.3" @@ -3440,6 +5259,17 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "ua-parser" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c06b979bd5606d182759ff9cd3dda2b034b584a1ed41116407cb92abf3c995a" +dependencies = [ + "regex", + "regex-filtered", + "serde", +] + [[package]] name = "ucd-trie" version = "0.1.7" @@ -3452,7 +5282,7 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" dependencies = [ - "rand", + "rand 0.9.2", "web-time", ] @@ -3468,12 +5298,76 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-width" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + [[package]] name = "utf-8" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.18.1" @@ -3485,6 +5379,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "uuid-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" +dependencies = [ + "outref", + "uuid", + "vsimd", +] + [[package]] name = "valuable" version = "0.1.1" @@ -3514,6 +5419,122 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vrl" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374906b686967832d85e5ce8f43581924e8382787e538c61c1e2158e09fc6518" +dependencies = [ + "aes", + "aes-siv", + "base16", + "base62", + "base64-simd", + "bytes", + "cbc", + "cfb-mode", + "cfg-if", + "chacha20poly1305", + "charset", + "chrono", + "chrono-tz", + "ciborium", + "cidr", + "clap", + "codespan-reporting", + "community-id", + "convert_case 0.7.1", + "crc", + "crypto_secretbox", + "csv", + "ctr", + "digest", + "dns-lookup", + "domain", + "dyn-clone", + "encoding_rs", + "fancy-regex 0.15.0", + "flate2", + "grok", + "hex", + "hmac", + "hostname", + "iana-time-zone", + "idna", + "indexmap 2.11.4", + "indoc", + "influxdb-line-protocol", + "ipcrypt-rs", + "itertools 0.14.0", + "jsonschema", + "lalrpop", + "lalrpop-util", + "lz4_flex", + "md-5", + "nom 8.0.0", + "nom-language", + "ofb", + "onig", + "ordered-float", + "parse-size", + "peeking_take_while", + "percent-encoding", + "pest", + "pest_derive", + "prost", + "prost-reflect", + "psl", + "psl-types", + "publicsuffix", + "quoted_printable", + "rand 0.8.5", + "regex", + "reqwest", + "reqwest-middleware", + "reqwest-retry", + "roxmltree", + "rust_decimal", + "seahash", + "serde", + "serde_json", + "serde_yaml", + "sha-1", + "sha2", + "sha3", + "simdutf8", + "snafu 0.8.9", + "snap", + "strip-ansi-escapes", + "syslog_loose", + "termcolor", + "thiserror 2.0.17", + "tokio", + "tracing", + "ua-parser", + "unicode-segmentation", + "url", + "utf8-width", + "uuid", + "woothee", + "xxhash-rust", + "zstd", +] + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -3653,6 +5674,21 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "wasm-timer" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" +dependencies = [ + "futures", + "js-sys", + "parking_lot 0.11.2", + "pin-utils", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.81" @@ -3712,7 +5748,7 @@ checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.2.0", "windows-result", "windows-strings", ] @@ -3739,6 +5775,12 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + [[package]] name = "windows-link" version = "0.2.0" @@ -3751,7 +5793,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" dependencies = [ - "windows-link", + "windows-link 0.2.0", ] [[package]] @@ -3760,7 +5802,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" dependencies = [ - "windows-link", + "windows-link 0.2.0", ] [[package]] @@ -3769,7 +5811,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -3778,7 +5820,16 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.4", ] [[package]] @@ -3787,7 +5838,7 @@ version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" dependencies = [ - "windows-link", + "windows-link 0.2.0", ] [[package]] @@ -3796,14 +5847,31 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" +dependencies = [ + "windows-link 0.2.0", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", ] [[package]] @@ -3812,48 +5880,96 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winnow" version = "0.7.13" @@ -3869,6 +5985,22 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "woothee" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "896174c6a4779d4d7d4523dd27aef7d46609eda2497e370f6c998325c6bf6971" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + [[package]] name = "xxhash-rust" version = "0.8.15" @@ -3886,6 +6018,30 @@ dependencies = [ "hashlink", ] +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.27" @@ -3905,3 +6061,97 @@ dependencies = [ "quote", "syn 2.0.106", ] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "zlib-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/bench/subgraphs/main.rs b/bench/subgraphs/main.rs index 99d3440ca..496cf5149 100644 --- a/bench/subgraphs/main.rs +++ b/bench/subgraphs/main.rs @@ -30,6 +30,21 @@ async fn delay_middleware(req: Request, next: Next) -> Response { next.run(req).await } +async fn add_subgraph_header(req: Request, next: Next) -> Response { + let path = req.uri().path(); + let subgraph_name = path.trim_start_matches('/').to_string(); + + let mut response = next.run(req).await; + + if !subgraph_name.is_empty() && subgraph_name != "health" { + if let Ok(header_value) = subgraph_name.parse() { + response.headers_mut().insert("x-subgraph", header_value); + } + } + + response +} + async fn health_check_handler() -> impl IntoResponse { StatusCode::OK } @@ -57,6 +72,7 @@ async fn main() { post_service(GraphQL::new(reviews::get_subgraph())), ) .route("/health", get(health_check_handler)) + .route_layer(middleware::from_fn(add_subgraph_header)) .route_layer(middleware::from_fn(delay_middleware)); println!("Starting server on http://localhost:4200"); diff --git a/bin/router/src/lib.rs b/bin/router/src/lib.rs index fc4846297..f440f0707 100644 --- a/bin/router/src/lib.rs +++ b/bin/router/src/lib.rs @@ -36,7 +36,7 @@ pub async fn router_entrypoint() -> Result<(), Box> { let supergraph_sdl = router_config.supergraph.load().await?; let parsed_schema = parse_schema(&supergraph_sdl); let addr = router_config.http.address(); - let shared_state = RouterSharedState::new(parsed_schema, router_config); + let shared_state = RouterSharedState::new(parsed_schema, router_config)?; web::HttpServer::new(move || { web::App::new() diff --git a/bin/router/src/pipeline/execution.rs b/bin/router/src/pipeline/execution.rs index 8e1262285..1c902097c 100644 --- a/bin/router/src/pipeline/execution.rs +++ b/bin/router/src/pipeline/execution.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::collections::HashMap; use std::sync::Arc; @@ -6,11 +7,13 @@ use crate::pipeline::error::{PipelineError, PipelineErrorFromAcceptHeader, Pipel use crate::pipeline::normalize::GraphQLNormalizationPayload; use crate::shared_state::RouterSharedState; use hive_router_plan_executor::execute_query_plan; -use hive_router_plan_executor::execution::plan::QueryPlanExecutionContext; +use hive_router_plan_executor::execution::plan::{ + ClientRequestDetails, OperationDetails, PlanExecutionOutput, QueryPlanExecutionContext, +}; use hive_router_plan_executor::introspection::resolve::IntrospectionContext; use hive_router_query_planner::planner::plan_nodes::QueryPlan; +use hive_router_query_planner::state::supergraph_state::OperationKind; use http::HeaderName; -use ntex::util::Bytes; use ntex::web::HttpRequest; static EXPOSE_QUERY_PLAN_HEADER: HeaderName = HeaderName::from_static("hive-expose-query-plan"); @@ -23,13 +26,14 @@ enum ExposeQueryPlanMode { } #[inline] -pub async fn execute_plan( +pub async fn execute_plan<'a>( req: &mut HttpRequest, + query: Cow<'a, str>, app_state: &Arc, normalized_payload: &Arc, query_plan_payload: &Arc, variable_payload: &CoerceVariablesPayload, -) -> Result { +) -> Result { let mut expose_query_plan = ExposeQueryPlanMode::No; if app_state.router_config.query_planner.allow_expose { @@ -64,14 +68,29 @@ pub async fn execute_plan( execute_query_plan(QueryPlanExecutionContext { query_plan: query_plan_payload, projection_plan: &normalized_payload.projection_plan, + headers_plan: &app_state.headers_plan, variable_values: &variable_payload.variables_map, extensions, + client_request: ClientRequestDetails { + method: req.method().clone(), + url: req.uri().clone(), + headers: req.headers(), + operation: OperationDetails { + name: normalized_payload.operation_for_plan.name.clone(), + kind: match normalized_payload.operation_for_plan.operation_kind { + Some(OperationKind::Query) => "query", + Some(OperationKind::Mutation) => "mutation", + Some(OperationKind::Subscription) => "subscription", + None => "query", + }, + query, + }, + }, introspection_context: &introspection_context, operation_type_name: normalized_payload.root_type_name, executors: &app_state.subgraph_executor_map, }) .await - .map(Bytes::from) .map_err(|err| { tracing::error!("Failed to execute query plan: {}", err); req.new_pipeline_error(PipelineErrorVariant::PlanExecutionError(err)) diff --git a/bin/router/src/pipeline/mod.rs b/bin/router/src/pipeline/mod.rs index f234f9ccc..671730d52 100644 --- a/bin/router/src/pipeline/mod.rs +++ b/bin/router/src/pipeline/mod.rs @@ -1,5 +1,6 @@ -use std::sync::Arc; +use std::{borrow::Cow, sync::Arc}; +use hive_router_plan_executor::execution::plan::PlanExecutionOutput; use hive_router_query_planner::utils::cancellation::CancellationToken; use http::{header::CONTENT_TYPE, HeaderValue, Method}; use ntex::{ @@ -52,7 +53,10 @@ pub async fn graphql_request_handler( } match execute_pipeline(req, body_bytes, state).await { - Ok(response_bytes) => { + Ok(response) => { + let response_bytes = Bytes::from(response.body); + let response_headers = response.headers; + let response_content_type: &'static HeaderValue = if req.accepts_content_type(*APPLICATION_GRAPHQL_RESPONSE_JSON_STR) { &APPLICATION_GRAPHQL_RESPONSE_JSON @@ -60,7 +64,14 @@ pub async fn graphql_request_handler( &APPLICATION_JSON }; - web::HttpResponse::Ok() + let mut response_builder = web::HttpResponse::Ok(); + for (header_name, header_value) in response_headers { + if let Some(header_name) = header_name { + response_builder.header(header_name, header_value); + } + } + + response_builder .header(http::header::CONTENT_TYPE, response_content_type) .body(response_bytes) } @@ -73,7 +84,7 @@ pub async fn execute_pipeline( req: &mut HttpRequest, body_bytes: Bytes, state: &Arc, -) -> Result { +) -> Result { let execution_request = get_execution_request(req, body_bytes).await?; let parser_payload = parse_operation_with_cache(req, state, &execution_request).await?; validate_operation_with_cache(req, state, &parser_payload).await?; @@ -81,6 +92,7 @@ pub async fn execute_pipeline( let progressive_override_ctx = request_override_context()?; let normalize_payload = normalize_request_with_cache(req, state, &execution_request, &parser_payload).await?; + let query = Cow::Owned(execution_request.query.clone()); let variable_payload = coerce_request_variables(req, state, execution_request, &normalize_payload)?; @@ -98,6 +110,7 @@ pub async fn execute_pipeline( let execution_result = execute_plan( req, + query, state, &normalize_payload, &query_plan_payload, diff --git a/bin/router/src/shared_state.rs b/bin/router/src/shared_state.rs index a5e6fa1e5..2f7a93ca1 100644 --- a/bin/router/src/shared_state.rs +++ b/bin/router/src/shared_state.rs @@ -4,6 +4,9 @@ use graphql_parser::schema::Document; use graphql_tools::validation::{utils::ValidationError, validate::ValidationPlan}; use hive_router_config::HiveRouterConfig; use hive_router_plan_executor::{ + headers::{ + compile::compile_headers_plan, errors::HeaderRuleCompileError, plan::HeaderRulesPlan, + }, introspection::schema::{SchemaMetadata, SchemaWithMetadata}, SubgraphExecutorMap, }; @@ -25,13 +28,14 @@ pub struct RouterSharedState { pub parse_cache: Cache>>, pub normalize_cache: Cache>, pub router_config: HiveRouterConfig, + pub headers_plan: HeaderRulesPlan, } impl RouterSharedState { pub fn new( parsed_supergraph_sdl: Document<'static, String>, router_config: HiveRouterConfig, - ) -> Arc { + ) -> Result, SharedStateError> { let supergraph_state = SupergraphState::new(&parsed_supergraph_sdl); let planner = Planner::new_from_supergraph(&parsed_supergraph_sdl).expect("failed to create planner"); @@ -43,16 +47,23 @@ impl RouterSharedState { ) .expect("Failed to create subgraph executor map"); - Arc::new(Self { + Ok(Arc::new(Self { schema_metadata, planner, validation_plan: graphql_tools::validation::rules::default_rules_validation_plan(), + headers_plan: compile_headers_plan(&router_config.headers).map_err(Box::new)?, subgraph_executor_map, plan_cache: moka::future::Cache::new(1000), validate_cache: moka::future::Cache::new(1000), parse_cache: moka::future::Cache::new(1000), normalize_cache: moka::future::Cache::new(1000), router_config, - }) + })) } } + +#[derive(thiserror::Error, Debug)] +pub enum SharedStateError { + #[error("invalid headers config: {0}")] + HeaderRuleCompileError(#[from] Box), +} diff --git a/docs/README.md b/docs/README.md index c2f2f0c8c..22033bbca 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,16 +4,18 @@ |Name|Type|Description|Required| |----|----|-----------|--------| +|[**headers**](#headers)|`object`|Configuration for the headers.
Default: `{}`
|| |[**http**](#http)|`object`|Configuration for the HTTP server/listener.
Default: `{"host":"0.0.0.0","port":4000}`
|| |[**log**](#log)|`object`|The router logger configuration.
Default: `{"filter":null,"format":"json","level":"info"}`
|| |[**query\_planner**](#query_planner)|`object`|Query planning configuration.
Default: `{"allow_expose":false,"timeout":"10s"}`
|| |[**supergraph**](#supergraph)|`object`|Configuration for the Federation supergraph source. By default, the router will use a local file-based supergraph source (`./supergraph.graphql`).
Default: `{"path":"supergraph.graphql","source":"file"}`
|| -|[**traffic\_shaping**](#traffic_shaping)|`object`|Configuration for the traffic-shaper executor. Use these configurations to control how requests are being executed to subgraphs.
Default: `{"dedupe_enabled":true,"dedupe_fingerprint_headers":["authorization"],"max_connections_per_host":100,"pool_idle_timeout_seconds":50}`
|| +|[**traffic\_shaping**](#traffic_shaping)|`object`|Configuration for the traffic-shaper executor. Use these configurations to control how requests are being executed to subgraphs.
Default: `{"dedupe_enabled":true,"max_connections_per_host":100,"pool_idle_timeout_seconds":50}`
|| **Additional Properties:** not allowed **Example** ```yaml +headers: {} http: host: 0.0.0.0 port: 4000 @@ -29,13 +31,1175 @@ supergraph: source: file traffic_shaping: dedupe_enabled: true - dedupe_fingerprint_headers: - - authorization max_connections_per_host: 100 pool_idle_timeout_seconds: 50 ``` + +## headers: object + +Configuration for the headers. + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|[**all**](#headersall)|`object`, `null`|Rules applied to all subgraphs (global defaults).
|| +|[**subgraphs**](#headerssubgraphs)|`object`, `null`|Rules applied to individual subgraphs.
|| + + +### headers\.all: object,null + +Rules applied to all subgraphs (global defaults). + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|[**request**](#headersallrequest)|`array`|Rules that shape the **request** sent from the router to subgraphs.
|| +|[**response**](#headersallresponse)|`array`|Rules that shape the **response** sent from the router back to the client.
|| + + +#### headers\.all\.request\[\]: array,null + +Rules that shape the **request** sent from the router to subgraphs. + + +**Items** + + +Request-header rules (applied before sending to a subgraph). + +  +**Option 1 (alternative):** +Forward headers from the client request into the subgraph request. + +- If `rename` is set, the header is forwarded under the new name. +- If **none** of the matched headers exist, `default` is used (when provided). + +**Order matters:** You can propagate first and then `remove` or `insert` +to refine the final output. + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|[**propagate**](#option1propagate)|`object`|Propagate headers from the client request to subgraph requests.
|yes| + +**Additional Properties:** not allowed +**Example** + +```yaml +propagate: + default: null + exclude: null + matching: null + named: null + rename: null + +``` + + +  +**Option 2 (alternative):** +Remove headers before sending the request to a subgraph. + +Useful to drop sensitive or irrelevant headers, or to undo a previous +`propagate`/`insert`. + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|[**remove**](#option2remove)|`object`|Remove headers matched by the specification.
|yes| + +**Additional Properties:** not allowed +**Example** + +```yaml +remove: + exclude: null + matching: null + named: null + +``` + + +  +**Option 3 (alternative):** +Add or overwrite a header with a static value. + +- For **normal** headers: replaces any existing value. +- For **never-join** headers (e.g. `set-cookie`): **appends** another + occurrence (multiple lines), never comma-joins. + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|[**insert**](#option3insert)|`object`|Insert a header with a static value.
|yes| + +**Additional Properties:** not allowed +**Example** + +```yaml +insert: {} + +``` + + +**Example** + +```yaml +{} + +``` + + +## Option 1: propagate: object + +Propagate headers from the client request to subgraph requests. + +**Behavior** +- If `rename` is provided, forwarded under that name. +- If **none** of the matched headers are present, `default` (when present) + is used under `rename` (if set) or the **first** `named` header. + +### Examples +```yaml +# Forward a specific header, but rename it per subgraph +propagate: + named: x-tenant-id + rename: x-acct-tenant + +# Forward all x- headers except legacy ones +propagate: + matching: "^x-.*" + exclude: ["^x-legacy-.*"] + +# If Authorization is missing, inject a default token for this subgraph +propagate: + named: Authorization + default: "Bearer test-token" +``` + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|**default**|`string`, `null`|If the header is missing, set a default value.
Applied only when **none** of the matched headers exist.
|| +|[**exclude**](#option1propagateexclude)|`string[]`|Exclude headers matching these regexes, applied after `matching`.
|| +|**matching**||Match headers by regex pattern(s) (OR).
|| +|**named**||Match headers by exact name (OR).
|| +|**rename**|`string`, `null`|Optionally rename the header when forwarding.
|| + +**Example** + +```yaml +default: null +exclude: null +matching: null +named: null +rename: null + +``` + + +### Option 1: propagate\.exclude\[\]: array,null + +Exclude headers matching these regexes, applied after `matching`. + + +**Items** + +**Item Type:** `string` +**Example** + +```yaml +{} + +``` + + +## Option 2: remove: object + +Remove headers matched by the specification. + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|[**exclude**](#option2removeexclude)|`string[]`|Exclude headers matching these regexes, applied after `matching`.
|| +|**matching**||Match headers by regex pattern(s) (OR).
|| +|**named**||Match headers by exact name (OR).
|| + +**Example** + +```yaml +exclude: null +matching: null +named: null + +``` + + +### Option 2: remove\.exclude\[\]: array,null + +Exclude headers matching these regexes, applied after `matching`. + + +**Items** + +**Item Type:** `string` +**Example** + +```yaml +{} + +``` + + +## Option 3: insert: object + +Insert a header with a static value. + +### Examples +```yaml +- insert: + name: x-env + value: prod +``` + +```yaml +- insert: + name: set-cookie + value: "a=1; Path=/" +# If another Set-Cookie exists, this creates another header line (never joined) +``` + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|**name**|`string`|Header name to insert or overwrite (case-insensitive).
|yes| + +  +**Option 1 (optional):** +Static value provided in the config. + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|**value**|`string`||yes| + + +  +**Option 2 (optional):** +A dynamic value computed by a VRL expression. + +This allows you to generate header values based on the incoming request, +subgraph name, and (for response rules) subgraph response headers. +The expression has access to a context object with `.request`, `.subgraph`, +and `.response` fields. + +For more information on the available functions and syntax, see the +[VRL documentation](https://vrl.dev/). + +### Example +```yaml +# Insert a header with a value derived from another header. +- insert: + name: x-auth-scheme + expression: 'split(.request.headers.authorization, " ")[0] ?? "none"' +``` + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|**expression**|`string`||yes| + + + +#### headers\.all\.response\[\]: array,null + +Rules that shape the **response** sent from the router back to the client. + + +**Items** + + +Response-header rules (applied before sending back to the client). + +  +**Option 1 (alternative):** +Forward headers from subgraph responses into the final client response. + +- If multiple subgraphs provide the same header, `algorithm` controls + how values are merged. +- If **no** subgraph provides a matching header, `default` is used (when provided). +- If `rename` is set, the header is returned under the new name. + +**Never-join headers** (e.g. `set-cookie`) are never comma-joined: +multiple values are returned as separate header fields regardless of `algorithm`. + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|[**propagate**](#option1propagate)|`object`|Propagate headers from subgraph responses to the final client response.
|yes| + +**Additional Properties:** not allowed +**Example** + +```yaml +propagate: + default: null + exclude: null + matching: null + named: null + rename: null + +``` + + +  +**Option 2 (alternative):** +Remove headers before sending the response to the client. + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|[**remove**](#option2remove)|`object`|Remove headers matched by the specification.
|yes| + +**Additional Properties:** not allowed +**Example** + +```yaml +remove: + exclude: null + matching: null + named: null + +``` + + +  +**Option 3 (alternative):** +Add or overwrite a header in the response to the client. + +For never-join headers, appends another occurrence (multiple lines). + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|[**insert**](#option3insert)|`object`|Insert a header with a static value.
|yes| + +**Additional Properties:** not allowed +**Example** + +```yaml +insert: + algorithm: null + +``` + + +**Example** + +```yaml +{} + +``` + + +## Option 1: propagate: object + +Propagate headers from subgraph responses to the final client response. + +**Behavior** +- If multiple subgraphs return the header, values are merged using `algorithm`. + Never-join headers are **never** comma-joined. +- If **no** subgraph returns a match, `default` (if set) is emitted. +- If `rename` is set, the outgoing header uses the new name. + +### Examples +```yaml +# Forward Cache-Control from whichever subgraph supplies it (last wins) +propagate: + named: Cache-Control + algorithm: last + +# Combine list-valued headers +propagate: + named: vary + algorithm: append + +# Ensure a fallback header is always present +propagate: + named: x-backend + algorithm: append + default: unknown +``` + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|**algorithm**||How to merge values across multiple subgraph responses.
|yes| +|**default**|`string`, `null`|If no subgraph returns the header, set this default value.
|no| +|[**exclude**](#option1propagateexclude)|`string[]`|Exclude headers matching these regexes, applied after `matching`.
|no| +|**matching**||Match headers by regex pattern(s) (OR).
|no| +|**named**||Match headers by exact name (OR).
|no| +|**rename**|`string`, `null`|Optionally rename the header when returning it to the client.
|no| + +**Example** + +```yaml +default: null +exclude: null +matching: null +named: null +rename: null + +``` + + +### Option 1: propagate\.exclude\[\]: array,null + +Exclude headers matching these regexes, applied after `matching`. + + +**Items** + +**Item Type:** `string` +**Example** + +```yaml +{} + +``` + + +## Option 2: remove: object + +Remove headers matched by the specification. + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|[**exclude**](#option2removeexclude)|`string[]`|Exclude headers matching these regexes, applied after `matching`.
|| +|**matching**||Match headers by regex pattern(s) (OR).
|| +|**named**||Match headers by exact name (OR).
|| + +**Example** + +```yaml +exclude: null +matching: null +named: null + +``` + + +### Option 2: remove\.exclude\[\]: array,null + +Exclude headers matching these regexes, applied after `matching`. + + +**Items** + +**Item Type:** `string` +**Example** + +```yaml +{} + +``` + + +## Option 3: insert: object + +Insert a header with a static value. + +### Examples +```yaml +- insert: + name: x-env + value: prod +``` + +```yaml +- insert: + name: set-cookie + value: "a=1; Path=/" +# If another Set-Cookie exists, this creates another header line (never joined) +``` + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|**algorithm**||How to merge values across multiple subgraph responses.
Default: `Last` (overwrite).
|no| +|**name**|`string`|Header name to insert or overwrite (case-insensitive).
|yes| + +  +**Option 1 (optional):** +Static value provided in the config. + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|**value**|`string`||yes| + + +  +**Option 2 (optional):** +A dynamic value computed by a VRL expression. + +This allows you to generate header values based on the incoming request, +subgraph name, and (for response rules) subgraph response headers. +The expression has access to a context object with `.request`, `.subgraph`, +and `.response` fields. + +For more information on the available functions and syntax, see the +[VRL documentation](https://vrl.dev/). + +### Example +```yaml +# Insert a header with a value derived from another header. +- insert: + name: x-auth-scheme + expression: 'split(.request.headers.authorization, " ")[0] ?? "none"' +``` + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|**expression**|`string`||yes| + + +**Example** + +```yaml +algorithm: null + +``` + + +### headers\.subgraphs: object,null + +Rules applied to individual subgraphs. +Keys are subgraph names as defined in the supergraph schema. + +**Precedence:** These are applied **after** `all`, and therefore can +override the result of global rules for that subgraph. + + +**Additional Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|[**Additional Properties**](#headerssubgraphsadditionalproperties)|`object`|Rules for a single scope (global or per-subgraph).
|| + + +#### headers\.subgraphs\.additionalProperties: object + +Rules for a single scope (global or per-subgraph). + +You can specify independent rule lists for **request** (to subgraphs) +and **response** (to clients). Within each list, rules are applied in order. + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|[**request**](#headerssubgraphsadditionalpropertiesrequest)|`array`|Rules that shape the **request** sent from the router to subgraphs.
|| +|[**response**](#headerssubgraphsadditionalpropertiesresponse)|`array`|Rules that shape the **response** sent from the router back to the client.
|| + +**Example** + +```yaml +request: null +response: null + +``` + + +##### headers\.subgraphs\.additionalProperties\.request\[\]: array,null + +Rules that shape the **request** sent from the router to subgraphs. + + +**Items** + + +Request-header rules (applied before sending to a subgraph). + +  +**Option 1 (alternative):** +Forward headers from the client request into the subgraph request. + +- If `rename` is set, the header is forwarded under the new name. +- If **none** of the matched headers exist, `default` is used (when provided). + +**Order matters:** You can propagate first and then `remove` or `insert` +to refine the final output. + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|[**propagate**](#option1propagate)|`object`|Propagate headers from the client request to subgraph requests.
|yes| + +**Additional Properties:** not allowed +**Example** + +```yaml +propagate: + default: null + exclude: null + matching: null + named: null + rename: null + +``` + + +  +**Option 2 (alternative):** +Remove headers before sending the request to a subgraph. + +Useful to drop sensitive or irrelevant headers, or to undo a previous +`propagate`/`insert`. + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|[**remove**](#option2remove)|`object`|Remove headers matched by the specification.
|yes| + +**Additional Properties:** not allowed +**Example** + +```yaml +remove: + exclude: null + matching: null + named: null + +``` + + +  +**Option 3 (alternative):** +Add or overwrite a header with a static value. + +- For **normal** headers: replaces any existing value. +- For **never-join** headers (e.g. `set-cookie`): **appends** another + occurrence (multiple lines), never comma-joins. + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|[**insert**](#option3insert)|`object`|Insert a header with a static value.
|yes| + +**Additional Properties:** not allowed +**Example** + +```yaml +insert: {} + +``` + + +**Example** + +```yaml +{} + +``` + + +## Option 1: propagate: object + +Propagate headers from the client request to subgraph requests. + +**Behavior** +- If `rename` is provided, forwarded under that name. +- If **none** of the matched headers are present, `default` (when present) + is used under `rename` (if set) or the **first** `named` header. + +### Examples +```yaml +# Forward a specific header, but rename it per subgraph +propagate: + named: x-tenant-id + rename: x-acct-tenant + +# Forward all x- headers except legacy ones +propagate: + matching: "^x-.*" + exclude: ["^x-legacy-.*"] + +# If Authorization is missing, inject a default token for this subgraph +propagate: + named: Authorization + default: "Bearer test-token" +``` + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|**default**|`string`, `null`|If the header is missing, set a default value.
Applied only when **none** of the matched headers exist.
|| +|[**exclude**](#option1propagateexclude)|`string[]`|Exclude headers matching these regexes, applied after `matching`.
|| +|**matching**||Match headers by regex pattern(s) (OR).
|| +|**named**||Match headers by exact name (OR).
|| +|**rename**|`string`, `null`|Optionally rename the header when forwarding.
|| + +**Example** + +```yaml +default: null +exclude: null +matching: null +named: null +rename: null + +``` + + +### Option 1: propagate\.exclude\[\]: array,null + +Exclude headers matching these regexes, applied after `matching`. + + +**Items** + +**Item Type:** `string` +**Example** + +```yaml +{} + +``` + + +## Option 2: remove: object + +Remove headers matched by the specification. + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|[**exclude**](#option2removeexclude)|`string[]`|Exclude headers matching these regexes, applied after `matching`.
|| +|**matching**||Match headers by regex pattern(s) (OR).
|| +|**named**||Match headers by exact name (OR).
|| + +**Example** + +```yaml +exclude: null +matching: null +named: null + +``` + + +### Option 2: remove\.exclude\[\]: array,null + +Exclude headers matching these regexes, applied after `matching`. + + +**Items** + +**Item Type:** `string` +**Example** + +```yaml +{} + +``` + + +## Option 3: insert: object + +Insert a header with a static value. + +### Examples +```yaml +- insert: + name: x-env + value: prod +``` + +```yaml +- insert: + name: set-cookie + value: "a=1; Path=/" +# If another Set-Cookie exists, this creates another header line (never joined) +``` + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|**name**|`string`|Header name to insert or overwrite (case-insensitive).
|yes| + +  +**Option 1 (optional):** +Static value provided in the config. + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|**value**|`string`||yes| + + +  +**Option 2 (optional):** +A dynamic value computed by a VRL expression. + +This allows you to generate header values based on the incoming request, +subgraph name, and (for response rules) subgraph response headers. +The expression has access to a context object with `.request`, `.subgraph`, +and `.response` fields. + +For more information on the available functions and syntax, see the +[VRL documentation](https://vrl.dev/). + +### Example +```yaml +# Insert a header with a value derived from another header. +- insert: + name: x-auth-scheme + expression: 'split(.request.headers.authorization, " ")[0] ?? "none"' +``` + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|**expression**|`string`||yes| + + + +##### headers\.subgraphs\.additionalProperties\.response\[\]: array,null + +Rules that shape the **response** sent from the router back to the client. + + +**Items** + + +Response-header rules (applied before sending back to the client). + +  +**Option 1 (alternative):** +Forward headers from subgraph responses into the final client response. + +- If multiple subgraphs provide the same header, `algorithm` controls + how values are merged. +- If **no** subgraph provides a matching header, `default` is used (when provided). +- If `rename` is set, the header is returned under the new name. + +**Never-join headers** (e.g. `set-cookie`) are never comma-joined: +multiple values are returned as separate header fields regardless of `algorithm`. + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|[**propagate**](#option1propagate)|`object`|Propagate headers from subgraph responses to the final client response.
|yes| + +**Additional Properties:** not allowed +**Example** + +```yaml +propagate: + default: null + exclude: null + matching: null + named: null + rename: null + +``` + + +  +**Option 2 (alternative):** +Remove headers before sending the response to the client. + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|[**remove**](#option2remove)|`object`|Remove headers matched by the specification.
|yes| + +**Additional Properties:** not allowed +**Example** + +```yaml +remove: + exclude: null + matching: null + named: null + +``` + + +  +**Option 3 (alternative):** +Add or overwrite a header in the response to the client. + +For never-join headers, appends another occurrence (multiple lines). + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|[**insert**](#option3insert)|`object`|Insert a header with a static value.
|yes| + +**Additional Properties:** not allowed +**Example** + +```yaml +insert: + algorithm: null + +``` + + +**Example** + +```yaml +{} + +``` + + +## Option 1: propagate: object + +Propagate headers from subgraph responses to the final client response. + +**Behavior** +- If multiple subgraphs return the header, values are merged using `algorithm`. + Never-join headers are **never** comma-joined. +- If **no** subgraph returns a match, `default` (if set) is emitted. +- If `rename` is set, the outgoing header uses the new name. + +### Examples +```yaml +# Forward Cache-Control from whichever subgraph supplies it (last wins) +propagate: + named: Cache-Control + algorithm: last + +# Combine list-valued headers +propagate: + named: vary + algorithm: append + +# Ensure a fallback header is always present +propagate: + named: x-backend + algorithm: append + default: unknown +``` + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|**algorithm**||How to merge values across multiple subgraph responses.
|yes| +|**default**|`string`, `null`|If no subgraph returns the header, set this default value.
|no| +|[**exclude**](#option1propagateexclude)|`string[]`|Exclude headers matching these regexes, applied after `matching`.
|no| +|**matching**||Match headers by regex pattern(s) (OR).
|no| +|**named**||Match headers by exact name (OR).
|no| +|**rename**|`string`, `null`|Optionally rename the header when returning it to the client.
|no| + +**Example** + +```yaml +default: null +exclude: null +matching: null +named: null +rename: null + +``` + + +### Option 1: propagate\.exclude\[\]: array,null + +Exclude headers matching these regexes, applied after `matching`. + + +**Items** + +**Item Type:** `string` +**Example** + +```yaml +{} + +``` + + +## Option 2: remove: object + +Remove headers matched by the specification. + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|[**exclude**](#option2removeexclude)|`string[]`|Exclude headers matching these regexes, applied after `matching`.
|| +|**matching**||Match headers by regex pattern(s) (OR).
|| +|**named**||Match headers by exact name (OR).
|| + +**Example** + +```yaml +exclude: null +matching: null +named: null + +``` + + +### Option 2: remove\.exclude\[\]: array,null + +Exclude headers matching these regexes, applied after `matching`. + + +**Items** + +**Item Type:** `string` +**Example** + +```yaml +{} + +``` + + +## Option 3: insert: object + +Insert a header with a static value. + +### Examples +```yaml +- insert: + name: x-env + value: prod +``` + +```yaml +- insert: + name: set-cookie + value: "a=1; Path=/" +# If another Set-Cookie exists, this creates another header line (never joined) +``` + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|**algorithm**||How to merge values across multiple subgraph responses.
Default: `Last` (overwrite).
|no| +|**name**|`string`|Header name to insert or overwrite (case-insensitive).
|yes| + +  +**Option 1 (optional):** +Static value provided in the config. + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|**value**|`string`||yes| + + +  +**Option 2 (optional):** +A dynamic value computed by a VRL expression. + +This allows you to generate header values based on the incoming request, +subgraph name, and (for response rules) subgraph response headers. +The expression has access to a context object with `.request`, `.subgraph`, +and `.response` fields. + +For more information on the available functions and syntax, see the +[VRL documentation](https://vrl.dev/). + +### Example +```yaml +# Insert a header with a value derived from another header. +- insert: + name: x-auth-scheme + expression: 'split(.request.headers.authorization, " ")[0] ?? "none"' +``` + + +**Properties** + +|Name|Type|Description|Required| +|----|----|-----------|--------| +|**expression**|`string`||yes| + + +**Example** + +```yaml +algorithm: null + +``` + ## http: object @@ -143,7 +1307,6 @@ Configuration for the traffic-shaper executor. Use these configurations to contr |Name|Type|Description|Required| |----|----|-----------|--------| |**dedupe\_enabled**|`boolean`|Enables/disables request deduplication to subgraphs.

When requests exactly matches the hashing mechanism (e.g., subgraph name, URL, headers, query, variables), and are executed at the same time, they will
be deduplicated by sharing the response of other in-flight requests.
Default: `true`
|| -|[**dedupe\_fingerprint\_headers**](#traffic_shapingdedupe_fingerprint_headers)|`string[]`|A list of headers that should be used to fingerprint requests for deduplication.
Default: `"authorization"`
|| |**max\_connections\_per\_host**|`integer`|Limits the concurrent amount of requests/connections per host/subgraph.
Default: `100`
Format: `"uint"`
Minimum: `0`
|| |**pool\_idle\_timeout\_seconds**|`integer`|Timeout for idle sockets being kept-alive.
Default: `50`
Format: `"uint64"`
Minimum: `0`
|| @@ -151,29 +1314,9 @@ Configuration for the traffic-shaper executor. Use these configurations to contr ```yaml dedupe_enabled: true -dedupe_fingerprint_headers: - - authorization max_connections_per_host: 100 pool_idle_timeout_seconds: 50 ``` - -### traffic\_shaping\.dedupe\_fingerprint\_headers\[\]: array - -A list of headers that should be used to fingerprint requests for deduplication. - -If not provided, the default is to use the "authorization" header only. - - -**Items** - -**Item Type:** `string` -**Example** - -```yaml -- authorization - -``` - diff --git a/lib/executor/Cargo.toml b/lib/executor/Cargo.toml index e353e8be7..513e9eda8 100644 --- a/lib/executor/Cargo.toml +++ b/lib/executor/Cargo.toml @@ -30,7 +30,10 @@ xxhash-rust = { workspace = true } tokio = { workspace = true, features = ["sync"] } dashmap = { workspace = true } ahash = "0.8.12" +regex-automata = "0.4.10" +vrl = { version = "0.27.0", features = ["compiler", "parser", "value", "diagnostic", "stdlib", "core"] } +ntex-http = "0.1.15" hyper-tls = { version = "0.6.0", features = ["vendored"] } hyper-util = { version = "0.1.16", features = [ "client", @@ -49,6 +52,7 @@ bumpalo = "3.19.0" subgraphs = { path = "../../bench/subgraphs" } criterion = { workspace = true } tokio = { workspace = true } +insta = { workspace = true } [[bench]] name = "executor_benches" diff --git a/lib/executor/src/context.rs b/lib/executor/src/context.rs index fa4aa220e..7982d3034 100644 --- a/lib/executor/src/context.rs +++ b/lib/executor/src/context.rs @@ -2,13 +2,17 @@ use std::collections::HashMap; use hive_router_query_planner::planner::plan_nodes::{FetchNode, FetchRewrite, QueryPlan}; -use crate::response::{graphql_error::GraphQLError, storage::ResponsesStorage, value::Value}; +use crate::{ + headers::plan::ResponseHeaderAggregator, + response::{graphql_error::GraphQLError, storage::ResponsesStorage, value::Value}, +}; pub struct ExecutionContext<'a> { pub response_storage: ResponsesStorage, pub final_response: Value<'a>, pub errors: Vec, pub output_rewrites: OutputRewritesStorage, + pub response_headers_aggregator: ResponseHeaderAggregator, } impl<'a> Default for ExecutionContext<'a> { @@ -18,6 +22,7 @@ impl<'a> Default for ExecutionContext<'a> { output_rewrites: Default::default(), errors: Vec::new(), final_response: Value::Null, + response_headers_aggregator: Default::default(), } } } @@ -29,6 +34,7 @@ impl<'a> ExecutionContext<'a> { output_rewrites: OutputRewritesStorage::from_query_plan(query_plan), errors: Vec::new(), final_response: init_final_response, + response_headers_aggregator: Default::default(), } } diff --git a/lib/executor/src/execution/error.rs b/lib/executor/src/execution/error.rs index 52b5d0bdb..7ee290c53 100644 --- a/lib/executor/src/execution/error.rs +++ b/lib/executor/src/execution/error.rs @@ -1,13 +1,9 @@ -use crate::projection::error::ProjectionError; +use crate::{headers::errors::HeaderRuleRuntimeError, projection::error::ProjectionError}; #[derive(thiserror::Error, Debug, Clone)] pub enum PlanExecutionError { #[error("Projection faiure: {0}")] - ProjectionFailure(ProjectionError), -} - -impl From for PlanExecutionError { - fn from(error: ProjectionError) -> Self { - PlanExecutionError::ProjectionFailure(error) - } + ProjectionFailure(#[from] ProjectionError), + #[error(transparent)] + HeaderPropagation(#[from] HeaderRuleRuntimeError), } diff --git a/lib/executor/src/execution/plan.rs b/lib/executor/src/execution/plan.rs index 286320163..723d5e52b 100644 --- a/lib/executor/src/execution/plan.rs +++ b/lib/executor/src/execution/plan.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{borrow::Cow, collections::HashMap}; use bytes::{BufMut, Bytes}; use futures::{future::BoxFuture, stream::FuturesUnordered, StreamExt}; @@ -6,13 +6,23 @@ use hive_router_query_planner::planner::plan_nodes::{ ConditionNode, FetchNode, FetchRewrite, FlattenNode, FlattenNodePath, ParallelNode, PlanNode, QueryPlan, SequenceNode, }; +use http::{HeaderMap, Method}; +use ntex_http::HeaderMap as NtexHeaderMap; use serde::Deserialize; use sonic_rs::ValueRef; use crate::{ context::ExecutionContext, execution::{error::PlanExecutionError, rewrites::FetchRewriteExt}, - executors::{common::HttpExecutionRequest, map::SubgraphExecutorMap}, + executors::{ + common::{HttpExecutionRequest, HttpExecutionResponse}, + map::SubgraphExecutorMap, + }, + headers::{ + plan::HeaderRulesPlan, + request::modify_subgraph_request_headers, + response::{apply_subgraph_response_headers, modify_client_response_headers}, + }, introspection::{ resolve::{resolve_introspection, IntrospectionContext}, schema::SchemaMetadata, @@ -32,19 +42,39 @@ use crate::{ }, }; +pub struct OperationDetails<'a> { + pub name: Option, + pub query: Cow<'a, str>, + pub kind: &'a str, +} + +pub struct ClientRequestDetails<'a> { + pub method: Method, + pub url: http::Uri, + pub headers: &'a NtexHeaderMap, + pub operation: OperationDetails<'a>, +} + pub struct QueryPlanExecutionContext<'exec> { pub query_plan: &'exec QueryPlan, pub projection_plan: &'exec Vec, + pub headers_plan: &'exec HeaderRulesPlan, pub variable_values: &'exec Option>, pub extensions: Option>, + pub client_request: ClientRequestDetails<'exec>, pub introspection_context: &'exec IntrospectionContext<'exec, 'static>, pub operation_type_name: &'exec str, pub executors: &'exec SubgraphExecutorMap, } +pub struct PlanExecutionOutput { + pub body: Vec, + pub headers: HeaderMap, +} + pub async fn execute_query_plan<'exec>( ctx: QueryPlanExecutionContext<'exec>, -) -> Result, PlanExecutionError> { +) -> Result { let init_value = if let Some(introspection_query) = ctx.introspection_context.query { resolve_introspection(introspection_query, ctx.introspection_context) } else { @@ -52,22 +82,27 @@ pub async fn execute_query_plan<'exec>( }; let mut exec_ctx = ExecutionContext::new(ctx.query_plan, init_value); + let executor = Executor::new( + ctx.variable_values, + ctx.executors, + ctx.introspection_context.metadata, + &ctx.client_request, + ctx.headers_plan, + // Deduplicate subgraph requests only if the operation type is a query + ctx.operation_type_name == "Query", + ); if ctx.query_plan.node.is_some() { - let executor = Executor::new( - ctx.variable_values, - ctx.executors, - ctx.introspection_context.metadata, - // Deduplicate subgraph requests only if the operation type is a query - ctx.operation_type_name == "Query", - ); executor .execute(&mut exec_ctx, ctx.query_plan.node.as_ref()) - .await; + .await?; } + let mut response_headers = HeaderMap::new(); + modify_client_response_headers(exec_ctx.response_headers_aggregator, &mut response_headers)?; + let final_response = &exec_ctx.final_response; - project_by_operation( + let body = project_by_operation( final_response, exec_ctx.errors, &ctx.extensions, @@ -75,14 +110,20 @@ pub async fn execute_query_plan<'exec>( ctx.projection_plan, ctx.variable_values, exec_ctx.response_storage.estimate_final_response_size(), - ) - .map_err(|e| e.into()) + )?; + + Ok(PlanExecutionOutput { + body, + headers: response_headers, + }) } pub struct Executor<'exec> { variable_values: &'exec Option>, schema_metadata: &'exec SchemaMetadata, executors: &'exec SubgraphExecutorMap, + client_request: &'exec ClientRequestDetails<'exec>, + headers_plan: &'exec HeaderRulesPlan, dedupe_subgraph_requests: bool, } @@ -110,15 +151,22 @@ impl<'exec, T> ConcurrencyScope<'exec, T> { } } +struct SubgraphOutput { + body: Bytes, + headers: HeaderMap, +} + struct FetchJob { fetch_node_id: i64, - response: Bytes, + subgraph_name: String, + response: SubgraphOutput, } struct FlattenFetchJob { flatten_node_path: FlattenNodePath, - response: Bytes, + response: SubgraphOutput, fetch_node_id: i64, + subgraph_name: String, representation_hashes: Vec, representation_hash_to_index: HashMap, } @@ -129,12 +177,30 @@ enum ExecutionJob { None, } -impl From for Bytes { +impl From for SubgraphOutput { fn from(value: ExecutionJob) -> Self { match value { - ExecutionJob::Fetch(j) => j.response, - ExecutionJob::FlattenFetch(j) => j.response, - ExecutionJob::None => Bytes::new(), + ExecutionJob::Fetch(j) => Self { + body: j.response.body, + headers: j.response.headers, + }, + ExecutionJob::FlattenFetch(j) => Self { + body: j.response.body, + headers: j.response.headers, + }, + ExecutionJob::None => Self { + body: Bytes::new(), + headers: HeaderMap::new(), + }, + } + } +} + +impl From for SubgraphOutput { + fn from(res: HttpExecutionResponse) -> Self { + Self { + body: res.body, + headers: res.headers, } } } @@ -150,48 +216,73 @@ impl<'exec> Executor<'exec> { variable_values: &'exec Option>, executors: &'exec SubgraphExecutorMap, schema_metadata: &'exec SchemaMetadata, + client_request: &'exec ClientRequestDetails<'exec>, + headers_plan: &'exec HeaderRulesPlan, dedupe_subgraph_requests: bool, ) -> Self { Executor { variable_values, executors, schema_metadata, + client_request, + headers_plan, dedupe_subgraph_requests, } } - pub async fn execute(&self, ctx: &mut ExecutionContext<'exec>, plan: Option<&PlanNode>) { + pub async fn execute( + &self, + ctx: &mut ExecutionContext<'exec>, + plan: Option<&PlanNode>, + ) -> Result<(), PlanExecutionError> { match plan { Some(PlanNode::Fetch(node)) => self.execute_fetch_wave(ctx, node).await, Some(PlanNode::Parallel(node)) => self.execute_parallel_wave(ctx, node).await, Some(PlanNode::Sequence(node)) => self.execute_sequence_wave(ctx, node).await, // Plans produced by our Query Planner can only start with: Fetch, Sequence or Parallel. // Any other node type at the root is not supported, do nothing - Some(_) => (), + Some(_) => Ok(()), // An empty plan is valid, just do nothing - None => (), + None => Ok(()), } } - async fn execute_fetch_wave(&self, ctx: &mut ExecutionContext<'exec>, node: &FetchNode) { + async fn execute_fetch_wave( + &self, + ctx: &mut ExecutionContext<'exec>, + node: &FetchNode, + ) -> Result<(), PlanExecutionError> { match self.execute_fetch_node(node, None).await { Ok(result) => self.process_job_result(ctx, result), - Err(err) => ctx.errors.push(GraphQLError { - message: err.to_string(), - locations: None, - path: None, - extensions: None, - }), + Err(err) => { + ctx.errors.push(GraphQLError { + message: err.to_string(), + locations: None, + path: None, + extensions: None, + }); + Ok(()) + } } } - async fn execute_sequence_wave(&self, ctx: &mut ExecutionContext<'exec>, node: &SequenceNode) { + async fn execute_sequence_wave( + &self, + ctx: &mut ExecutionContext<'exec>, + node: &SequenceNode, + ) -> Result<(), PlanExecutionError> { for child in &node.nodes { - Box::pin(self.execute_plan_node(ctx, child)).await; + Box::pin(self.execute_plan_node(ctx, child)).await?; } + + Ok(()) } - async fn execute_parallel_wave(&self, ctx: &mut ExecutionContext<'exec>, node: &ParallelNode) { + async fn execute_parallel_wave( + &self, + ctx: &mut ExecutionContext<'exec>, + node: &ParallelNode, + ) -> Result<(), PlanExecutionError> { let mut scope = ConcurrencyScope::new(); for child in &node.nodes { @@ -204,7 +295,7 @@ impl<'exec> Executor<'exec> { for result in results { match result { Ok(job) => { - self.process_job_result(ctx, job); + self.process_job_result(ctx, job)?; } Err(err) => ctx.errors.push(GraphQLError { message: err.to_string(), @@ -214,13 +305,19 @@ impl<'exec> Executor<'exec> { }), } } + + Ok(()) } - async fn execute_plan_node(&self, ctx: &mut ExecutionContext<'exec>, node: &PlanNode) { + async fn execute_plan_node( + &self, + ctx: &mut ExecutionContext<'exec>, + node: &PlanNode, + ) -> Result<(), PlanExecutionError> { match node { PlanNode::Fetch(fetch_node) => match self.execute_fetch_node(fetch_node, None).await { Ok(job) => { - self.process_job_result(ctx, job); + self.process_job_result(ctx, job)?; } Err(err) => ctx.errors.push(GraphQLError { message: err.to_string(), @@ -230,7 +327,7 @@ impl<'exec> Executor<'exec> { }), }, PlanNode::Parallel(parallel_node) => { - self.execute_parallel_wave(ctx, parallel_node).await; + self.execute_parallel_wave(ctx, parallel_node).await?; } PlanNode::Flatten(flatten_node) => { match self.prepare_flatten_data(&ctx.final_response, flatten_node) { @@ -245,7 +342,7 @@ impl<'exec> Executor<'exec> { .await { Ok(job) => { - self.process_job_result(ctx, job); + self.process_job_result(ctx, job)?; } Err(err) => { ctx.errors.push(GraphQLError { @@ -269,18 +366,20 @@ impl<'exec> Executor<'exec> { } } PlanNode::Sequence(sequence_node) => { - self.execute_sequence_wave(ctx, sequence_node).await; + self.execute_sequence_wave(ctx, sequence_node).await?; } PlanNode::Condition(condition_node) => { if let Some(node) = condition_node_by_variables(condition_node, self.variable_values) { - Box::pin(self.execute_plan_node(ctx, node)).await; + Box::pin(self.execute_plan_node(ctx, node)).await?; } } // An unsupported plan node was found, do nothing. _ => {} } + + Ok(()) } fn prepare_job_future<'wave>( @@ -355,11 +454,23 @@ impl<'exec> Executor<'exec> { Some((response.data, output_rewrites)) } - fn process_job_result(&self, ctx: &mut ExecutionContext<'exec>, job: ExecutionJob) { - match job { + fn process_job_result( + &self, + ctx: &mut ExecutionContext<'exec>, + job: ExecutionJob, + ) -> Result<(), PlanExecutionError> { + let _: () = match job { ExecutionJob::Fetch(job) => { + apply_subgraph_response_headers( + self.headers_plan, + &job.subgraph_name, + &job.response.headers, + self.client_request, + &mut ctx.response_headers_aggregator, + )?; + if let Some((mut data, output_rewrites)) = - self.process_subgraph_response(ctx, job.response, job.fetch_node_id) + self.process_subgraph_response(ctx, job.response.body, job.fetch_node_id) { if let Some(output_rewrites) = output_rewrites { for output_rewrite in output_rewrites { @@ -371,8 +482,16 @@ impl<'exec> Executor<'exec> { } } ExecutionJob::FlattenFetch(job) => { + apply_subgraph_response_headers( + self.headers_plan, + &job.subgraph_name, + &job.response.headers, + self.client_request, + &mut ctx.response_headers_aggregator, + )?; + if let Some((mut data, output_rewrites)) = - self.process_subgraph_response(ctx, job.response, job.fetch_node_id) + self.process_subgraph_response(ctx, job.response.body, job.fetch_node_id) { if let Some(mut entities) = data.take_entities() { if let Some(output_rewrites) = output_rewrites { @@ -413,7 +532,8 @@ impl<'exec> Executor<'exec> { ExecutionJob::None => { // nothing to do } - } + }; + Ok(()) } fn prepare_flatten_data( @@ -510,6 +630,7 @@ impl<'exec> Executor<'exec> { .await? .into(), fetch_node_id: fetch_node.id, + subgraph_name: fetch_node.service_name.clone(), representation_hashes: representation_hashes.unwrap_or_default(), representation_hash_to_index: filtered_representations_hashes.unwrap_or_default(), }), @@ -522,8 +643,18 @@ impl<'exec> Executor<'exec> { node: &FetchNode, representations: Option>, ) -> Result { + // TODO: We could optimize header map creation by caching them per service name + let mut headers_map = HeaderMap::new(); + modify_subgraph_request_headers( + self.headers_plan, + &node.service_name, + self.client_request, + &mut headers_map, + )?; + Ok(ExecutionJob::Fetch(FetchJob { fetch_node_id: node.id, + subgraph_name: node.service_name.clone(), response: self .executors .execute( @@ -534,9 +665,11 @@ impl<'exec> Executor<'exec> { operation_name: node.operation_name.as_deref(), variables: None, representations, + headers: headers_map, }, ) - .await, + .await + .into(), })) } } diff --git a/lib/executor/src/executors/common.rs b/lib/executor/src/executors/common.rs index 7862a2c69..6a053bdd5 100644 --- a/lib/executor/src/executors/common.rs +++ b/lib/executor/src/executors/common.rs @@ -2,10 +2,14 @@ use std::{collections::HashMap, sync::Arc}; use async_trait::async_trait; use bytes::Bytes; +use http::HeaderMap; #[async_trait] pub trait SubgraphExecutor { - async fn execute<'a>(&self, execution_request: HttpExecutionRequest<'a>) -> Bytes; + async fn execute<'a>( + &self, + execution_request: HttpExecutionRequest<'a>, + ) -> HttpExecutionResponse; fn to_boxed_arc<'a>(self) -> Arc> where Self: Sized + Send + Sync + 'a, @@ -24,5 +28,11 @@ pub struct HttpExecutionRequest<'a> { pub operation_name: Option<&'a str>, // TODO: variables could be stringified before even executing the request pub variables: Option>, + pub headers: HeaderMap, pub representations: Option>, } + +pub struct HttpExecutionResponse { + pub body: Bytes, + pub headers: HeaderMap, +} diff --git a/lib/executor/src/executors/dedupe.rs b/lib/executor/src/executors/dedupe.rs index 8ab6b8448..a60599f19 100644 --- a/lib/executor/src/executors/dedupe.rs +++ b/lib/executor/src/executors/dedupe.rs @@ -2,7 +2,7 @@ use ahash::AHasher; use bytes::Bytes; use http::{HeaderMap, Method, StatusCode, Uri}; use std::collections::BTreeMap; -use std::hash::{BuildHasherDefault, Hash, Hasher}; +use std::hash::{BuildHasher, BuildHasherDefault, Hash, Hasher}; #[derive(Debug, Clone)] pub struct SharedResponse { @@ -11,66 +11,29 @@ pub struct SharedResponse { pub body: Bytes, } -#[derive(Debug, Clone, Eq)] -pub struct RequestFingerprint { - method: Method, - url: Uri, - /// BTreeMap to ensure case-insensitivity and consistent order for hashing - headers: BTreeMap, - body: Vec, -} - -impl RequestFingerprint { - pub fn new( - method: &Method, - url: &Uri, - req_headers: &HeaderMap, - body_bytes: &[u8], - fingerprint_headers: &[String], - ) -> Self { - let mut headers = BTreeMap::new(); - if fingerprint_headers.is_empty() { - // fingerprint all headers - for (key, value) in req_headers.iter() { - if let Ok(value_str) = value.to_str() { - headers.insert(key.as_str().to_lowercase(), value_str.to_string()); - } - } - } else { - for header_name in fingerprint_headers.iter() { - if let Some(value) = req_headers.get(header_name) { - if let Ok(value_str) = value.to_str() { - headers.insert(header_name.to_lowercase(), value_str.to_string()); - } - } - } - } +pub fn request_fingerprint( + method: &Method, + url: &Uri, + req_headers: &HeaderMap, + body_bytes: &[u8], +) -> u64 { + let build_hasher = ABuildHasher::default(); + let mut hasher = build_hasher.build_hasher(); - Self { - method: method.clone(), - url: url.clone(), - headers, - body: body_bytes.to_vec(), + // BTreeMap to ensure case-insensitivity and consistent order for hashing + let mut headers = BTreeMap::new(); + for (header_name, header_value) in req_headers.iter() { + if let Ok(value_str) = header_value.to_str() { + headers.insert(header_name.as_str(), value_str); } } -} -impl Hash for RequestFingerprint { - fn hash(&self, state: &mut H) { - self.method.hash(state); - self.url.hash(state); - self.headers.hash(state); - self.body.hash(state); - } -} + method.hash(&mut hasher); + url.hash(&mut hasher); + headers.hash(&mut hasher); + body_bytes.hash(&mut hasher); -impl PartialEq for RequestFingerprint { - fn eq(&self, other: &Self) -> bool { - self.method == other.method - && self.url == other.url - && self.headers == other.headers - && self.body == other.body - } + hasher.finish() } pub type ABuildHasher = BuildHasherDefault; diff --git a/lib/executor/src/executors/http.rs b/lib/executor/src/executors/http.rs index 559b3dd38..e1a285430 100644 --- a/lib/executor/src/executors/http.rs +++ b/lib/executor/src/executors/http.rs @@ -1,6 +1,7 @@ use std::sync::Arc; -use crate::executors::dedupe::{ABuildHasher, RequestFingerprint, SharedResponse}; +use crate::executors::common::HttpExecutionResponse; +use crate::executors::dedupe::{request_fingerprint, ABuildHasher, SharedResponse}; use dashmap::DashMap; use hive_router_config::traffic_shaping::TrafficShapingExecutorConfig; use tokio::sync::OnceCell; @@ -33,8 +34,7 @@ pub struct HTTPSubgraphExecutor { pub header_map: HeaderMap, pub semaphore: Arc, pub config: Arc, - pub in_flight_requests: - Arc>, ABuildHasher>>, + pub in_flight_requests: Arc>, ABuildHasher>>, } const FIRST_VARIABLE_STR: &[u8] = b",\"variables\":{"; @@ -46,13 +46,11 @@ impl HTTPSubgraphExecutor { http_client: Arc, Full>>, semaphore: Arc, config: Arc, - in_flight_requests: Arc< - DashMap>, ABuildHasher>, - >, + in_flight_requests: Arc>, ABuildHasher>>, ) -> Self { let mut header_map = HeaderMap::new(); header_map.insert( - "Content-Type", + http::header::CONTENT_TYPE, HeaderValue::from_static("application/json; charset=utf-8"), ); header_map.insert( @@ -118,7 +116,11 @@ impl HTTPSubgraphExecutor { Ok(body) } - async fn _send_request(&self, body: Vec) -> Result { + async fn _send_request( + &self, + body: Vec, + headers: HeaderMap, + ) -> Result { let mut req = hyper::Request::builder() .method(http::Method::POST) .uri(&self.endpoint) @@ -128,7 +130,7 @@ impl HTTPSubgraphExecutor { SubgraphExecutorError::RequestBuildFailure(self.endpoint.to_string(), e.to_string()) })?; - *req.headers_mut() = self.header_map.clone(); + *req.headers_mut() = headers; let res = self.http_client.request(req).await.map_err(|e| { SubgraphExecutorError::RequestFailure(self.endpoint.to_string(), e.to_string()) @@ -168,35 +170,48 @@ impl HTTPSubgraphExecutor { #[async_trait] impl SubgraphExecutor for HTTPSubgraphExecutor { - async fn execute<'a>(&self, execution_request: HttpExecutionRequest<'a>) -> Bytes { + async fn execute<'a>( + &self, + execution_request: HttpExecutionRequest<'a>, + ) -> HttpExecutionResponse { let body = match self.build_request_body(&execution_request) { Ok(body) => body, - Err(e) => return self.error_to_graphql_bytes(e), + Err(e) => { + return HttpExecutionResponse { + body: self.error_to_graphql_bytes(e), + headers: Default::default(), + } + } }; + let mut headers = execution_request.headers; + self.header_map.iter().for_each(|(key, value)| { + headers.insert(key, value.clone()); + }); + if !self.config.dedupe_enabled || !execution_request.dedupe { // This unwrap is safe because the semaphore is never closed during the application's lifecycle. // `acquire()` only fails if the semaphore is closed, so this will always return `Ok`. let _permit = self.semaphore.acquire().await.unwrap(); - return match self._send_request(body).await { - Ok(shared_response) => shared_response.body, - Err(e) => self.error_to_graphql_bytes(e), + return match self._send_request(body, headers).await { + Ok(shared_response) => HttpExecutionResponse { + body: shared_response.body, + headers: shared_response.headers, + }, + Err(e) => HttpExecutionResponse { + body: self.error_to_graphql_bytes(e), + headers: Default::default(), + }, }; } - let fingerprint = RequestFingerprint::new( - &http::Method::POST, - &self.endpoint, - &self.header_map, - &body, - &self.config.dedupe_fingerprint_headers, - ); + let fingerprint = request_fingerprint(&http::Method::POST, &self.endpoint, &headers, &body); // Clone the cell from the map, dropping the lock from the DashMap immediately. // Prevents any deadlocks. let cell = self .in_flight_requests - .entry(fingerprint.clone()) + .entry(fingerprint) .or_default() .value() .clone(); @@ -207,7 +222,7 @@ impl SubgraphExecutor for HTTPSubgraphExecutor { // This unwrap is safe because the semaphore is never closed during the application's lifecycle. // `acquire()` only fails if the semaphore is closed, so this will always return `Ok`. let _permit = self.semaphore.acquire().await.unwrap(); - self._send_request(body).await + self._send_request(body, headers).await }; // It's important to remove the entry from the map before returning the result. // This ensures that once the OnceCell is set, no future requests can join it. @@ -218,8 +233,14 @@ impl SubgraphExecutor for HTTPSubgraphExecutor { .await; match response_result { - Ok(shared_response) => shared_response.body.clone(), - Err(e) => self.error_to_graphql_bytes(e.clone()), + Ok(shared_response) => HttpExecutionResponse { + body: shared_response.body.clone(), + headers: shared_response.headers.clone(), + }, + Err(e) => HttpExecutionResponse { + body: self.error_to_graphql_bytes(e.clone()), + headers: Default::default(), + }, } } } diff --git a/lib/executor/src/executors/map.rs b/lib/executor/src/executors/map.rs index 69d7e7de5..ff18d118c 100644 --- a/lib/executor/src/executors/map.rs +++ b/lib/executor/src/executors/map.rs @@ -1,6 +1,6 @@ use std::{collections::HashMap, sync::Arc, time::Duration}; -use bytes::{BufMut, Bytes, BytesMut}; +use bytes::{BufMut, BytesMut}; use dashmap::DashMap; use hive_router_config::traffic_shaping::TrafficShapingExecutorConfig; use http::Uri; @@ -13,8 +13,10 @@ use tokio::sync::{OnceCell, Semaphore}; use crate::{ executors::{ - common::{HttpExecutionRequest, SubgraphExecutor, SubgraphExecutorBoxedArc}, - dedupe::{ABuildHasher, RequestFingerprint, SharedResponse}, + common::{ + HttpExecutionRequest, HttpExecutionResponse, SubgraphExecutor, SubgraphExecutorBoxedArc, + }, + dedupe::{ABuildHasher, SharedResponse}, error::SubgraphExecutorError, http::HTTPSubgraphExecutor, }, @@ -42,7 +44,7 @@ impl SubgraphExecutorMap { &self, subgraph_name: &str, execution_request: HttpExecutionRequest<'a>, - ) -> Bytes { + ) -> HttpExecutionResponse { match self.inner.get(subgraph_name) { Some(executor) => executor.execute(execution_request).await, None => { @@ -57,7 +59,11 @@ impl SubgraphExecutorMap { buffer.put_slice(b"{\"errors\":"); buffer.put_slice(&errors_bytes); buffer.put_slice(b"}"); - buffer.freeze() + + HttpExecutionResponse { + body: buffer.freeze(), + headers: Default::default(), + } } } } @@ -81,9 +87,8 @@ impl SubgraphExecutorMap { let semaphores_by_origin: DashMap> = DashMap::new(); let max_connections_per_host = config.max_connections_per_host; let config_arc = Arc::new(config); - let in_flight_requests: Arc< - DashMap>, ABuildHasher>, - > = Arc::new(DashMap::with_hasher(ABuildHasher::default())); + let in_flight_requests: Arc>, ABuildHasher>> = + Arc::new(DashMap::with_hasher(ABuildHasher::default())); let executor_map = subgraph_endpoint_map .into_iter() diff --git a/lib/executor/src/headers/compile.rs b/lib/executor/src/headers/compile.rs new file mode 100644 index 000000000..5b1b14a9f --- /dev/null +++ b/lib/executor/src/headers/compile.rs @@ -0,0 +1,484 @@ +use crate::headers::{ + errors::HeaderRuleCompileError, + plan::{ + HeaderAggregationStrategy, HeaderRulesPlan, RequestHeaderRule, RequestHeaderRules, + RequestInsertExpression, RequestInsertStatic, RequestPropagateNamed, RequestPropagateRegex, + RequestRemoveNamed, RequestRemoveRegex, ResponseHeaderRule, ResponseHeaderRules, + ResponseInsertExpression, ResponseInsertStatic, ResponsePropagateNamed, + ResponsePropagateRegex, ResponseRemoveNamed, ResponseRemoveRegex, + }, +}; + +use hive_router_config::headers as config; +use http::HeaderName; +use regex_automata::{meta, util::syntax::Config as SyntaxConfig}; +use vrl::{ + compiler::compile as vrl_compile, prelude::Function as VrlFunction, + stdlib::all as vrl_build_functions, +}; + +pub struct HeaderRuleCompilerContext { + vrl_functions: Vec>, +} + +impl Default for HeaderRuleCompilerContext { + fn default() -> Self { + Self::new() + } +} + +impl HeaderRuleCompilerContext { + pub fn new() -> Self { + Self { + vrl_functions: vrl_build_functions(), + } + } +} + +pub trait HeaderRuleCompiler { + fn compile( + &self, + ctx: &HeaderRuleCompilerContext, + actions: &mut A, + ) -> Result<(), HeaderRuleCompileError>; +} + +impl HeaderRuleCompiler> for config::RequestHeaderRule { + fn compile( + &self, + ctx: &HeaderRuleCompilerContext, + actions: &mut Vec, + ) -> Result<(), HeaderRuleCompileError> { + match self { + config::RequestHeaderRule::Propagate(rule) => { + let spec = materialize_match_spec( + &rule.spec, + rule.rename.as_ref(), + rule.default.as_ref(), + )?; + + if !spec.header_names.is_empty() { + actions.push(RequestHeaderRule::PropagateNamed(RequestPropagateNamed { + names: spec.header_names, + default: spec.default_header_value, + rename: spec.rename_header, + })); + } + if spec.include_regex.is_some() { + actions.push(RequestHeaderRule::PropagateRegex(RequestPropagateRegex { + include: spec.include_regex, + exclude: spec.exclude_regex, + })); + } + } + config::RequestHeaderRule::Insert(rule) => match &rule.source { + config::InsertSource::Value { value } => { + actions.push(RequestHeaderRule::InsertStatic(RequestInsertStatic { + name: build_header_name(&rule.name)?, + value: build_header_value(&rule.name, value)?, + })); + } + config::InsertSource::Expression { expression } => { + let compilation_result = + vrl_compile(expression, &ctx.vrl_functions).map_err(|e| { + HeaderRuleCompileError::new_expression_build(rule.name.clone(), e) + })?; + + actions.push(RequestHeaderRule::InsertExpression( + RequestInsertExpression { + name: build_header_name(&rule.name)?, + expression: Box::new(compilation_result.program), + }, + )); + } + }, + config::RequestHeaderRule::Remove(rule) => { + let spec = materialize_match_spec(&rule.spec, None, None)?; + if !spec.header_names.is_empty() { + actions.push(RequestHeaderRule::RemoveNamed(RequestRemoveNamed { + names: spec.header_names, + })); + } + if let Some(regex_set) = spec.include_regex { + actions.push(RequestHeaderRule::RemoveRegex(RequestRemoveRegex { + regex: regex_set, + })); + } + } + } + + Ok(()) + } +} + +impl HeaderRuleCompiler> for config::ResponseHeaderRule { + fn compile( + &self, + ctx: &HeaderRuleCompilerContext, + actions: &mut Vec, + ) -> Result<(), HeaderRuleCompileError> { + match self { + config::ResponseHeaderRule::Propagate(rule) => { + let aggregation_strategy = rule.algorithm.into(); + let spec = materialize_match_spec( + &rule.spec, + rule.rename.as_ref(), + rule.default.as_ref(), + )?; + + if !spec.header_names.is_empty() { + actions.push(ResponseHeaderRule::PropagateNamed(ResponsePropagateNamed { + names: spec.header_names, + rename: spec.rename_header, + default: spec.default_header_value, + strategy: aggregation_strategy, + })); + } + + if spec.include_regex.is_some() || spec.exclude_regex.is_some() { + actions.push(ResponseHeaderRule::PropagateRegex(ResponsePropagateRegex { + include: spec.include_regex, + exclude: spec.exclude_regex, + strategy: aggregation_strategy, + })); + } + } + config::ResponseHeaderRule::Insert(rule) => { + let aggregation_strategy = rule.algorithm.into(); + match &rule.source { + config::InsertSource::Value { value } => { + actions.push(ResponseHeaderRule::InsertStatic(ResponseInsertStatic { + name: build_header_name(&rule.name)?, + value: build_header_value(&rule.name, value)?, + strategy: aggregation_strategy, + })); + } + config::InsertSource::Expression { expression } => { + // NOTE: In case we ever need to improve performance and not pass the whole context + // to VRL expressions, we can use: + // - compilation_result.program.info().target_assignments + // - compilation_result.program.info().target_queries + // to determine what parts of the context are actually needed by the expression + let compilation_result = vrl_compile(expression, &ctx.vrl_functions) + .map_err(|e| { + HeaderRuleCompileError::new_expression_build(rule.name.clone(), e) + })?; + + actions.push(ResponseHeaderRule::InsertExpression( + ResponseInsertExpression { + name: build_header_name(&rule.name)?, + expression: Box::new(compilation_result.program), + strategy: aggregation_strategy, + }, + )); + } + } + } + config::ResponseHeaderRule::Remove(rule) => { + let spec = materialize_match_spec(&rule.spec, None, None)?; + if !spec.header_names.is_empty() { + actions.push(ResponseHeaderRule::RemoveNamed(ResponseRemoveNamed { + names: spec.header_names, + })); + } + if let Some(regex_set) = spec.include_regex { + actions.push(ResponseHeaderRule::RemoveRegex(ResponseRemoveRegex { + regex: regex_set, + })); + } + } + } + + Ok(()) + } +} + +pub fn compile_headers_plan( + cfg: &config::HeadersConfig, +) -> Result { + let ctx = HeaderRuleCompilerContext::new(); + let mut request_plan = RequestHeaderRules::default(); + let mut response_plan = ResponseHeaderRules::default(); + + if let Some(global_rules) = &cfg.all { + request_plan.global = compile_request_header_rules(&ctx, global_rules)?; + response_plan.global = compile_response_header_rules(&ctx, global_rules)?; + } + + if let Some(subgraph_rules_map) = &cfg.subgraphs { + for (subgraph_name, subgraph_rules) in subgraph_rules_map { + let request_actions = compile_request_header_rules(&ctx, subgraph_rules)?; + let response_actions = compile_response_header_rules(&ctx, subgraph_rules)?; + request_plan + .by_subgraph + .insert(subgraph_name.clone(), request_actions); + response_plan + .by_subgraph + .insert(subgraph_name.clone(), response_actions); + } + } + + Ok(HeaderRulesPlan { + request: request_plan, + response: response_plan, + }) +} + +fn compile_request_header_rules( + ctx: &HeaderRuleCompilerContext, + header_rules: &config::HeaderRules, +) -> Result, HeaderRuleCompileError> { + let mut request_actions = Vec::new(); + if let Some(request_rule_entries) = &header_rules.request { + for request_rule in request_rule_entries { + request_rule.compile(ctx, &mut request_actions)?; + } + } + Ok(request_actions) +} + +fn compile_response_header_rules( + ctx: &HeaderRuleCompilerContext, + header_rules: &config::HeaderRules, +) -> Result, HeaderRuleCompileError> { + let mut response_actions = Vec::new(); + if let Some(response_rule_entries) = &header_rules.response { + for response_rule in response_rule_entries { + response_rule.compile(ctx, &mut response_actions)?; + } + } + Ok(response_actions) +} + +struct HeaderMatchSpecResult { + header_names: Vec, + include_regex: Option, + exclude_regex: Option, + rename_header: Option, + default_header_value: Option, +} + +fn materialize_match_spec( + match_spec: &config::MatchSpec, + rename_to: Option<&String>, + default_value: Option<&String>, +) -> Result { + let header_names = match &match_spec.named { + Some(config::OneOrMany::One(single_name)) => vec![build_header_name(single_name)?], + Some(config::OneOrMany::Many(many_names)) => many_names + .iter() + .map(|name| build_header_name(name)) + .collect::, _>>()?, + None => Vec::new(), + }; + + let include_regex = match match_spec.matching.as_ref() { + None => None, + Some(config::OneOrMany::One(pattern)) => build_regex_many(std::slice::from_ref(pattern))?, + Some(config::OneOrMany::Many(pattern_vec)) => build_regex_many(pattern_vec)?, + }; + + let exclude_regex = match match_spec.exclude.as_deref() { + None => None, + Some(pattern_vec) => build_regex_many(pattern_vec)?, + }; + + let rename_header = rename_to + .map(|name| match header_names.len() == 1 { + true => build_header_name(name), + false => Err(HeaderRuleCompileError::InvalidRename), + }) + .transpose()?; + + let default_header_value = default_value + .map(|value| match header_names.len() == 1 { + true => build_header_value(header_names[0].as_str(), value), + false => Err(HeaderRuleCompileError::InvalidDefault), + }) + .transpose()?; + + Ok(HeaderMatchSpecResult { + header_names, + include_regex, + exclude_regex, + rename_header, + default_header_value, + }) +} + +fn build_header_name(header_name_str: &str) -> Result { + http::HeaderName::from_bytes(header_name_str.as_bytes()) + .map_err(|err| HeaderRuleCompileError::BadHeaderName(header_name_str.into(), err)) +} + +fn build_header_value( + header_name_str: &str, + header_value_str: &str, +) -> Result { + http::HeaderValue::from_str(header_value_str) + .map_err(|err| HeaderRuleCompileError::BadHeaderValue(header_name_str.to_string(), err)) +} + +fn build_regex_many(patterns: &[String]) -> Result, HeaderRuleCompileError> { + if patterns.is_empty() { + return Ok(None); + } + let mut regex_builder = meta::Regex::builder(); + regex_builder.syntax(SyntaxConfig::new().unicode(false).utf8(false)); + regex_builder + .build_many(patterns) + .map(Some) + .map_err(|e| Box::new(e).into()) +} + +impl From for HeaderAggregationStrategy { + fn from(algo: config::AggregationAlgo) -> Self { + match algo { + config::AggregationAlgo::First => HeaderAggregationStrategy::First, + config::AggregationAlgo::Last => HeaderAggregationStrategy::Last, + config::AggregationAlgo::Append => HeaderAggregationStrategy::Append, + } + } +} + +impl From> for HeaderAggregationStrategy { + fn from(algo: Option) -> Self { + match algo { + Some(config::AggregationAlgo::First) => HeaderAggregationStrategy::First, + Some(config::AggregationAlgo::Last) => HeaderAggregationStrategy::Last, + Some(config::AggregationAlgo::Append) => HeaderAggregationStrategy::Append, + None => HeaderAggregationStrategy::Last, + } + } +} + +#[cfg(test)] +mod tests { + use hive_router_config::headers as config; + use http::HeaderName; + + use crate::headers::{ + compile::{build_header_value, HeaderRuleCompiler, HeaderRuleCompilerContext}, + errors::HeaderRuleCompileError, + plan::{HeaderAggregationStrategy, RequestHeaderRule, ResponseHeaderRule}, + }; + + fn header_name_owned(s: &str) -> HeaderName { + HeaderName::from_bytes(s.as_bytes()).unwrap() + } + + #[test] + fn test_propagate_named_request() { + let rule = config::RequestHeaderRule::Propagate(config::RequestPropagateRule { + spec: config::MatchSpec { + named: Some(config::OneOrMany::One("x-test".to_string())), + matching: None, + exclude: None, + }, + rename: None, + default: None, + }); + let ctx = HeaderRuleCompilerContext::new(); + let mut actions = Vec::new(); + rule.compile(&ctx, &mut actions).unwrap(); + assert_eq!(actions.len(), 1); + match &actions[0] { + RequestHeaderRule::PropagateNamed(data) => { + assert_eq!(data.names, vec![header_name_owned("x-test")]); + assert!(data.default.is_none()); + assert!(data.rename.is_none()); + } + _ => panic!("Expected PropagateNamed"), + } + } + + #[test] + fn test_set_request() { + let rule = config::RequestHeaderRule::Insert(config::RequestInsertRule { + name: "x-set".to_string(), + source: config::InsertSource::Value { + value: "abc".to_string(), + }, + }); + let mut actions = Vec::new(); + let ctx = HeaderRuleCompilerContext::new(); + rule.compile(&ctx, &mut actions).unwrap(); + assert_eq!(actions.len(), 1); + match &actions[0] { + RequestHeaderRule::InsertStatic(data) => { + assert_eq!(data.name, header_name_owned("x-set")); + assert_eq!(data.value, build_header_value("x-set", "abc").unwrap()); + } + _ => panic!("Expected SetStatic"), + } + } + + #[test] + fn test_remove_named_request() { + let rule = config::RequestHeaderRule::Remove(config::RemoveRule { + spec: config::MatchSpec { + named: Some(config::OneOrMany::One("x-remove".to_string())), + matching: None, + exclude: None, + }, + }); + let mut actions = Vec::new(); + let ctx = HeaderRuleCompilerContext::new(); + rule.compile(&ctx, &mut actions).unwrap(); + assert_eq!(actions.len(), 1); + match &actions[0] { + RequestHeaderRule::RemoveNamed(data) => { + assert_eq!(data.names, vec![header_name_owned("x-remove")]); + } + _ => panic!("Expected RemoveNamed"), + } + } + + #[test] + fn test_invalid_default_request() { + let rule = config::RequestHeaderRule::Propagate(config::RequestPropagateRule { + spec: config::MatchSpec { + named: Some(config::OneOrMany::Many(vec![ + "x1".to_string(), + "x2".to_string(), + ])), + matching: None, + exclude: None, + }, + rename: None, + default: Some("def".to_string()), + }); + let mut actions = Vec::new(); + let ctx = HeaderRuleCompilerContext::new(); + let err = rule.compile(&ctx, &mut actions).unwrap_err(); + match err { + HeaderRuleCompileError::InvalidDefault => {} + _ => panic!("Expected InvalidDefault error"), + } + } + + #[test] + fn test_propagate_named_response() { + let rule = config::ResponseHeaderRule::Propagate(config::ResponsePropagateRule { + spec: config::MatchSpec { + named: Some(config::OneOrMany::One("x-resp".to_string())), + matching: None, + exclude: None, + }, + rename: None, + default: None, + algorithm: config::AggregationAlgo::First, + }); + let mut actions = Vec::new(); + let ctx = HeaderRuleCompilerContext::new(); + rule.compile(&ctx, &mut actions).unwrap(); + assert_eq!(actions.len(), 1); + match &actions[0] { + ResponseHeaderRule::PropagateNamed(data) => { + assert_eq!(data.names, vec![header_name_owned("x-resp")]); + assert!(matches!(data.strategy, HeaderAggregationStrategy::First)); + } + _ => panic!("Expected PropagateNamed"), + } + } +} diff --git a/lib/executor/src/headers/errors.rs b/lib/executor/src/headers/errors.rs new file mode 100644 index 000000000..d53444877 --- /dev/null +++ b/lib/executor/src/headers/errors.rs @@ -0,0 +1,47 @@ +use http::header::{InvalidHeaderName, InvalidHeaderValue}; +use regex_automata::meta::BuildError; +use vrl::{diagnostic::DiagnosticList, prelude::ExpressionError}; + +#[derive(thiserror::Error, Debug)] +pub enum HeaderRuleCompileError { + #[error("Invalid header name '{0}'. Please check the configuration. Reason: {1}")] + BadHeaderName(String, InvalidHeaderName), + #[error("Invalid header value for header '{0}'. Please check the configuration. Reason: {1}")] + BadHeaderValue(String, InvalidHeaderValue), + #[error("The 'rename' option is only allowed when propagating a single header specified with 'named'. You cannot use 'rename' when propagating multiple headers or when using 'matching'.")] + InvalidRename, + #[error("The 'default' option is only allowed when propagating a single header specified with 'named'. You cannot use 'default' when propagating multiple headers or when using 'matching'.")] + InvalidDefault, + #[error("Failed to build regex for header matching. Please check your regex patterns for syntax errors. Reason: {0}")] + RegexBuild(#[from] Box), + #[error("Failed to compile VRL expression for header '{0}'. Please check your VRL expression for syntax errors. Diagnostic: {1}")] + ExpressionBuild(String, String), +} + +#[derive(thiserror::Error, Debug, Clone)] +pub enum HeaderRuleRuntimeError { + #[error("Failed to evaluate VRL expression for header '{0}'. Reason: {1}")] + ExpressionEvaluation(String, Box), + #[error("Invalid header value for header '{0}'.")] + BadHeaderValue(String), +} + +impl HeaderRuleCompileError { + pub fn new_expression_build(header_name: String, diagnostics: DiagnosticList) -> Self { + HeaderRuleCompileError::ExpressionBuild( + header_name, + diagnostics + .errors() + .into_iter() + .map(|d| d.code.to_string() + ": " + &d.message) + .collect::>() + .join(", "), + ) + } +} + +impl HeaderRuleRuntimeError { + pub fn new_expression_evaluation(header_name: String, error: Box) -> Self { + HeaderRuleRuntimeError::ExpressionEvaluation(header_name, error) + } +} diff --git a/lib/executor/src/headers/expression.rs b/lib/executor/src/headers/expression.rs new file mode 100644 index 000000000..541863304 --- /dev/null +++ b/lib/executor/src/headers/expression.rs @@ -0,0 +1,147 @@ +use std::collections::BTreeMap; + +use bytes::Bytes; +use http::{HeaderMap, HeaderValue}; +use tracing::warn; +use vrl::core::Value; + +use crate::{ + execution::plan::ClientRequestDetails, + headers::{request::RequestExpressionContext, response::ResponseExpressionContext}, +}; + +fn warn_unsupported_conversion_option(type_name: &str) -> Option { + warn!( + "Cannot convert VRL {} value to a header value. Please convert it to a string first.", + type_name + ); + + None +} + +pub fn vrl_value_to_header_value(value: Value) -> Option { + match value { + Value::Bytes(bytes) => HeaderValue::from_bytes(&bytes).ok(), + Value::Float(f) => HeaderValue::from_str(&f.to_string()).ok(), + Value::Boolean(b) => HeaderValue::from_str(if b { "true" } else { "false" }).ok(), + Value::Integer(i) => HeaderValue::from_str(&i.to_string()).ok(), + Value::Array(_) => warn_unsupported_conversion_option("Array"), + Value::Regex(_) => warn_unsupported_conversion_option("Regex"), + Value::Timestamp(_) => warn_unsupported_conversion_option("Timestamp"), + Value::Object(_) => warn_unsupported_conversion_option("Object"), + Value::Null => { + warn!("Cannot convert VRL Null value to a header value."); + None + } + } +} + +fn header_map_to_vrl_value(headers: &HeaderMap) -> Value { + let mut obj = BTreeMap::new(); + for (header_name, header_value) in headers.iter() { + if let Ok(value) = header_value.to_str() { + obj.insert( + header_name.as_str().into(), + Value::Bytes(Bytes::from(value.to_owned())), + ); + } + } + Value::Object(obj) +} + +fn client_header_map_to_vrl_value(headers: &ntex_http::HeaderMap) -> Value { + let mut obj = BTreeMap::new(); + for (header_name, header_value) in headers.iter() { + if let Ok(value) = header_value.to_str() { + obj.insert( + header_name.as_str().into(), + Value::Bytes(Bytes::from(value.to_owned())), + ); + } + } + Value::Object(obj) +} + +impl From<&RequestExpressionContext<'_>> for Value { + /// NOTE: If performance becomes an issue, consider pre-computing parts of this context that do not change + fn from(ctx: &RequestExpressionContext) -> Self { + // .subgraph + let subgraph_value = { + let value = Self::Bytes(Bytes::from(ctx.subgraph_name.to_owned())); + Self::Object(BTreeMap::from([("name".into(), value)])) + }; + + // .request + let request_value: Self = ctx.client_request.into(); + + Self::Object(BTreeMap::from([ + ("subgraph".into(), subgraph_value), + ("request".into(), request_value), + ])) + } +} + +impl From<&ResponseExpressionContext<'_>> for Value { + /// NOTE: If performance becomes an issue, consider pre-computing parts of this context that do not change + fn from(ctx: &ResponseExpressionContext) -> Self { + // .subgraph + let subgraph_value = Self::Object(BTreeMap::from([( + "name".into(), + Self::Bytes(Bytes::from(ctx.subgraph_name.to_owned())), + )])); + // .response + let response_value = header_map_to_vrl_value(ctx.subgraph_headers); + // .request + let request_value: Self = ctx.client_request.into(); + + Self::Object(BTreeMap::from([ + ("subgraph".into(), subgraph_value), + ("response".into(), response_value), + ("request".into(), request_value), + ])) + } +} + +impl From<&ClientRequestDetails<'_>> for Value { + fn from(details: &ClientRequestDetails) -> Self { + // .request.headers + let headers_value = client_header_map_to_vrl_value(details.headers); + + // .request.url + let url_value = Self::Object(BTreeMap::from([ + ( + "host".into(), + details.url.host().unwrap_or("unknown").into(), + ), + ("path".into(), details.url.path().into()), + ( + "port".into(), + details + .url + .port_u16() + .unwrap_or_else(|| { + if details.url.scheme() == Some(&http::uri::Scheme::HTTPS) { + 443 + } else { + 80 + } + }) + .into(), + ), + ])); + + // .request.operation + let operation_value = Self::Object(BTreeMap::from([ + ("name".into(), details.operation.name.clone().into()), + ("type".into(), details.operation.kind.into()), + ("query".into(), details.operation.query.clone().into()), + ])); + + Self::Object(BTreeMap::from([ + ("method".into(), details.method.as_str().into()), + ("headers".into(), headers_value), + ("url".into(), url_value), + ("operation".into(), operation_value), + ])) + } +} diff --git a/lib/executor/src/headers/mod.rs b/lib/executor/src/headers/mod.rs new file mode 100644 index 000000000..f38445de2 --- /dev/null +++ b/lib/executor/src/headers/mod.rs @@ -0,0 +1,621 @@ +pub mod compile; +pub mod errors; +pub mod expression; +pub mod plan; +pub mod request; +pub mod response; +pub mod sanitizer; + +#[cfg(test)] +mod tests { + use crate::{ + execution::plan::{ClientRequestDetails, OperationDetails}, + headers::{ + compile::compile_headers_plan, + plan::ResponseHeaderAggregator, + request::modify_subgraph_request_headers, + response::{apply_subgraph_response_headers, modify_client_response_headers}, + }, + }; + use hive_router_config::parse_yaml_config; + use http::{HeaderMap, HeaderName, HeaderValue}; + use ntex_http::HeaderMap as NtexHeaderMap; + + fn header_name_owned(s: &str) -> HeaderName { + HeaderName::from_bytes(s.as_bytes()).unwrap() + } + fn header_value_owned(s: &str) -> HeaderValue { + HeaderValue::from_str(s).unwrap() + } + + trait HeaderMapAsStringExt { + fn to_string(&self) -> String; + } + + impl HeaderMapAsStringExt for HeaderMap { + fn to_string(&self) -> String { + let mut buffer = String::new(); + + for (name, value) in self.iter() { + buffer.push_str(&format!( + "{}: {}\n", + name.as_str(), + value.to_str().unwrap_or("") + )); + } + + buffer + } + } + + #[test] + fn test_build_subgraph_headers_propagate_and_set() { + let yaml_str = r#" + headers: + all: + request: + - propagate: + named: x-prop + rename: x-renamed + - insert: + name: x-set + value: set-value + "#; + let config = parse_yaml_config(String::from(yaml_str)).unwrap(); + + let plan = compile_headers_plan(&config.headers).unwrap(); + + let mut client_headers = NtexHeaderMap::new(); + client_headers.insert( + header_name_owned("x-prop"), + header_value_owned("abc").into(), + ); + + let client_details = ClientRequestDetails { + method: http::Method::POST, + url: "http://example.com".parse().unwrap(), + headers: &client_headers, + operation: OperationDetails { + name: None, + query: "{ __typename }".to_string().into(), + kind: "query", + }, + }; + + let mut out = HeaderMap::new(); + modify_subgraph_request_headers(&plan, "any", &client_details, &mut out).unwrap(); + + insta::assert_snapshot!(out.to_string(), @r#" + x-renamed: abc + x-set: set-value + "#); + } + + #[test] + fn test_build_subgraph_headers_with_default() { + let yaml_str = r#" + headers: + all: + request: + - propagate: + named: x-missing + default: default-value + "#; + let config = parse_yaml_config(String::from(yaml_str)).unwrap(); + let plan = compile_headers_plan(&config.headers).unwrap(); + let client_headers = NtexHeaderMap::new(); + let client_details = ClientRequestDetails { + method: http::Method::POST, + url: "http://example.com".parse().unwrap(), + headers: &client_headers, + operation: OperationDetails { + name: None, + query: "{ __typename }".to_string().into(), + kind: "query", + }, + }; + let mut out = HeaderMap::new(); + modify_subgraph_request_headers(&plan, "any", &client_details, &mut out).unwrap(); + + insta::assert_snapshot!(out.to_string(), @r#" + x-missing: default-value + "#); + } + + // Tests that `matching` and `exclude` rules are correctly applied for propagation. + #[test] + fn test_propagate_with_matching_and_exclude() { + let yaml_str = r#" + headers: + all: + request: + - propagate: + matching: "^x-.*" + exclude: ["^x-secret-.*"] + "#; + let config = parse_yaml_config(String::from(yaml_str)).unwrap(); + let plan = compile_headers_plan(&config.headers).unwrap(); + + let mut client_headers = NtexHeaderMap::new(); + client_headers.insert( + header_name_owned("x-forward-this"), + header_value_owned("value1").into(), + ); + client_headers.insert( + header_name_owned("x-secret-header"), + header_value_owned("value2").into(), + ); + client_headers.insert( + header_name_owned("authorization"), + header_value_owned("value3").into(), + ); + + let client_details = ClientRequestDetails { + method: http::Method::POST, + url: "http://example.com".parse().unwrap(), + headers: &client_headers, + operation: OperationDetails { + name: None, + query: "{ __typename }".to_string().into(), + kind: "query", + }, + }; + + let mut out = HeaderMap::new(); + modify_subgraph_request_headers(&plan, "any", &client_details, &mut out).unwrap(); + + assert_eq!(out.get("x-forward-this").unwrap(), "value1"); + assert!(out.get("x-secret-header").is_none()); + assert!(out.get("authorization").is_none()); + + insta::assert_snapshot!(out.to_string(), @r#" + x-forward-this: value1 + "#); + } + + // Tests inserting a header with a value from a VRL expression. + #[test] + fn test_insert_request_header_with_expression() { + let yaml_str = r#" + headers: + all: + request: + - insert: + name: x-operation-name + expression: '.request.operation.name || "unknown"' + "#; + let config = parse_yaml_config(String::from(yaml_str)).unwrap(); + let plan = compile_headers_plan(&config.headers).unwrap(); + let client_headers = NtexHeaderMap::new(); + let client_details = ClientRequestDetails { + method: http::Method::POST, + url: "http://example.com".parse().unwrap(), + headers: &client_headers, + operation: OperationDetails { + name: Some("MyQuery".to_string()), + query: "{ __typename }".to_string().into(), + kind: "query", + }, + }; + + let mut out = HeaderMap::new(); + modify_subgraph_request_headers(&plan, "any", &client_details, &mut out).unwrap(); + + insta::assert_snapshot!(out.to_string(), @r#" + x-operation-name: MyQuery + "#); + } + + // Tests VRL expression fallback to a default value when a field is null. + #[test] + fn test_insert_request_header_with_expression_fallback() { + let yaml_str = r#" + headers: + all: + request: + - insert: + name: x-operation-name + expression: '.request.operation.name || "unknown"' + "#; + let config = parse_yaml_config(String::from(yaml_str)).unwrap(); + let plan = compile_headers_plan(&config.headers).unwrap(); + let client_headers = NtexHeaderMap::new(); + let client_details = ClientRequestDetails { + method: http::Method::POST, + url: "http://example.com".parse().unwrap(), + headers: &client_headers, + operation: OperationDetails { + name: None, + query: "{ __typename }".to_string().into(), + kind: "query", + }, + }; + + let mut out = HeaderMap::new(); + modify_subgraph_request_headers(&plan, "any", &client_details, &mut out).unwrap(); + + insta::assert_snapshot!(out.to_string(), @r#" + x-operation-name: unknown + "#); + } + + // Tests that subgraph-specific rules override global `all` rules. + #[test] + fn test_subgraph_specific_request_rules() { + let yaml_str = r#" + headers: + all: + request: + - insert: + name: x-scope + value: all + subgraphs: + accounts: + request: + - insert: + name: x-scope + value: accounts + "#; + let config = parse_yaml_config(String::from(yaml_str)).unwrap(); + let plan = compile_headers_plan(&config.headers).unwrap(); + let client_headers = NtexHeaderMap::new(); + let client_details = ClientRequestDetails { + method: http::Method::POST, + url: "http://example.com".parse().unwrap(), + headers: &client_headers, + operation: OperationDetails { + name: None, + query: "{ __typename }".to_string().into(), + kind: "query", + }, + }; + + // For "accounts" subgraph, the specific rule should apply. + let mut out_accounts = HeaderMap::new(); + modify_subgraph_request_headers(&plan, "accounts", &client_details, &mut out_accounts) + .unwrap(); + + insta::assert_snapshot!(out_accounts.to_string(), @r#" + x-scope: accounts + "#); + + // For any other subgraph, the `all` rule should apply. + let mut out_other = HeaderMap::new(); + modify_subgraph_request_headers(&plan, "products", &client_details, &mut out_other) + .unwrap(); + + insta::assert_snapshot!(out_other.to_string(), @r#" + x-scope: all + "#); + } + + #[test] + fn test_apply_subgraph_response_headers_and_finalize() { + let yaml_str = r#" + headers: + all: + response: + - propagate: + named: x-resp + algorithm: last + "#; + let config = parse_yaml_config(String::from(yaml_str)).unwrap(); + let plan = compile_headers_plan(&config.headers).unwrap(); + let client_headers = NtexHeaderMap::new(); + let client_details = ClientRequestDetails { + method: http::Method::POST, + url: "http://example.com".parse().unwrap(), + headers: &client_headers, + operation: OperationDetails { + name: None, + query: "{ __typename }".to_string().into(), + kind: "query", + }, + }; + + let mut accumulator = ResponseHeaderAggregator::default(); + + let mut subgraph_headers = HeaderMap::new(); + subgraph_headers.insert( + header_name_owned("x-resp"), + header_value_owned("resp-value-1"), + ); + apply_subgraph_response_headers( + &plan, + "any", + &subgraph_headers, + &client_details, + &mut accumulator, + ) + .unwrap(); + + let mut subgraph_headers = HeaderMap::new(); + subgraph_headers.insert( + header_name_owned("x-resp"), + header_value_owned("resp-value-2"), + ); + + apply_subgraph_response_headers( + &plan, + "any", + &subgraph_headers, + &client_details, + &mut accumulator, + ) + .unwrap(); + + let mut final_headers = HeaderMap::new(); + modify_client_response_headers(accumulator, &mut final_headers).unwrap(); + + insta::assert_snapshot!(final_headers.to_string(), @r#" + x-resp: resp-value-2 + "#); + } + + // Tests the `first` algorithm for response header propagation. + #[test] + fn test_response_propagate_first() { + let yaml_str = r#" + headers: + all: + response: + - propagate: + named: x-resp + algorithm: first + "#; + let config = parse_yaml_config(String::from(yaml_str)).unwrap(); + let plan = compile_headers_plan(&config.headers).unwrap(); + let client_headers = NtexHeaderMap::new(); + let client_details = ClientRequestDetails { + method: http::Method::POST, + url: "http://example.com".parse().unwrap(), + headers: &client_headers, + operation: OperationDetails { + name: None, + query: "{ __typename }".to_string().into(), + kind: "query", + }, + }; + + let mut accumulator = ResponseHeaderAggregator::default(); + + let mut subgraph_headers_1 = HeaderMap::new(); + subgraph_headers_1.insert( + header_name_owned("x-resp"), + header_value_owned("resp-value-1"), + ); + apply_subgraph_response_headers( + &plan, + "any", + &subgraph_headers_1, + &client_details, + &mut accumulator, + ) + .unwrap(); + + let mut subgraph_headers_2 = HeaderMap::new(); + subgraph_headers_2.insert( + header_name_owned("x-resp"), + header_value_owned("resp-value-2"), + ); + apply_subgraph_response_headers( + &plan, + "any", + &subgraph_headers_2, + &client_details, + &mut accumulator, + ) + .unwrap(); + + let mut final_headers = HeaderMap::new(); + modify_client_response_headers(accumulator, &mut final_headers).unwrap(); + + insta::assert_snapshot!(final_headers.to_string(), @r#" + x-resp: resp-value-1 + "#); + } + + // Tests the `append` algorithm for response header propagation. + #[test] + fn test_response_propagate_append() { + let yaml_str = r#" + headers: + all: + response: + - propagate: + named: x-stuff + algorithm: append + "#; + let config = parse_yaml_config(String::from(yaml_str)).unwrap(); + let plan = compile_headers_plan(&config.headers).unwrap(); + let client_headers = NtexHeaderMap::new(); + let client_details = ClientRequestDetails { + method: http::Method::POST, + url: "http://example.com".parse().unwrap(), + headers: &client_headers, + operation: OperationDetails { + name: None, + query: "{ __typename }".to_string().into(), + kind: "query", + }, + }; + let mut accumulator = ResponseHeaderAggregator::default(); + + let mut subgraph1_headers = HeaderMap::new(); + subgraph1_headers.insert(header_name_owned("x-stuff"), header_value_owned("val1")); + apply_subgraph_response_headers( + &plan, + "subgraph1", + &subgraph1_headers, + &client_details, + &mut accumulator, + ) + .unwrap(); + + let mut subgraph2_headers = HeaderMap::new(); + subgraph2_headers.insert(header_name_owned("x-stuff"), header_value_owned("val2")); + apply_subgraph_response_headers( + &plan, + "subgraph2", + &subgraph2_headers, + &client_details, + &mut accumulator, + ) + .unwrap(); + + let mut final_headers = HeaderMap::new(); + modify_client_response_headers(accumulator, &mut final_headers).unwrap(); + + insta::assert_snapshot!(final_headers.to_string(), @r#" + x-stuff: val1, val2 + "#); + } + + // Tests that "never-join" headers like set-cookie are appended as separate fields. + #[test] + fn test_response_propagate_append_never_join() { + let yaml_str = r#" + headers: + all: + response: + - propagate: + named: set-cookie + algorithm: append + "#; + let config = parse_yaml_config(String::from(yaml_str)).unwrap(); + let plan = compile_headers_plan(&config.headers).unwrap(); + let client_headers = NtexHeaderMap::new(); + let client_details = ClientRequestDetails { + method: http::Method::POST, + url: "http://example.com".parse().unwrap(), + headers: &client_headers, + operation: OperationDetails { + name: None, + query: "{ __typename }".to_string().into(), + kind: "query", + }, + }; + let mut accumulator = ResponseHeaderAggregator::default(); + + let mut subgraph1_headers = HeaderMap::new(); + subgraph1_headers.insert(header_name_owned("set-cookie"), header_value_owned("a=1")); + apply_subgraph_response_headers( + &plan, + "subgraph1", + &subgraph1_headers, + &client_details, + &mut accumulator, + ) + .unwrap(); + + let mut subgraph2_headers = HeaderMap::new(); + subgraph2_headers.insert(header_name_owned("set-cookie"), header_value_owned("b=2")); + apply_subgraph_response_headers( + &plan, + "subgraph2", + &subgraph2_headers, + &client_details, + &mut accumulator, + ) + .unwrap(); + + let mut final_headers = HeaderMap::new(); + modify_client_response_headers(accumulator, &mut final_headers).unwrap(); + + insta::assert_snapshot!(final_headers.to_string(), @r#" + set-cookie: a=1 + set-cookie: b=2 + "#); + } + + // Tests inserting a response header with a value from a VRL expression. + #[test] + fn test_insert_response_header_with_expression() { + let yaml_str = r#" + headers: + all: + response: + - insert: + name: x-original-forwarded-for + expression: '.response."x-forwarded-for"' + "#; + let config = parse_yaml_config(String::from(yaml_str)).unwrap(); + let plan = compile_headers_plan(&config.headers).unwrap(); + let client_headers = NtexHeaderMap::new(); + let client_details = ClientRequestDetails { + method: http::Method::POST, + url: "http://example.com".parse().unwrap(), + headers: &client_headers, + operation: OperationDetails { + name: None, + query: "{ __typename }".to_string().into(), + kind: "query", + }, + }; + + let mut accumulator = ResponseHeaderAggregator::default(); + + let mut subgraph_headers = HeaderMap::new(); + subgraph_headers.insert( + header_name_owned("x-forwarded-for"), + header_value_owned("1.2.3.4"), + ); + + apply_subgraph_response_headers( + &plan, + "any", + &subgraph_headers, + &client_details, + &mut accumulator, + ) + .unwrap(); + + let mut final_headers = HeaderMap::new(); + modify_client_response_headers(accumulator, &mut final_headers).unwrap(); + + insta::assert_snapshot!(final_headers.to_string(), @r#" + x-original-forwarded-for: 1.2.3.4 + "#); + } + + #[test] + fn test_remove_header() { + let yaml_str = r#" + headers: + all: + request: + - propagate: + named: x-keep + - remove: + named: x-remove + "#; + let config = parse_yaml_config(String::from(yaml_str)).unwrap(); + let plan = compile_headers_plan(&config.headers).unwrap(); + + let mut client_headers = NtexHeaderMap::new(); + + client_headers.insert( + header_name_owned("x-remove"), + header_value_owned("bye").into(), + ); + client_headers.insert(header_name_owned("x-keep"), header_value_owned("hi").into()); + + let client_details = ClientRequestDetails { + method: http::Method::POST, + url: "http://example.com".parse().unwrap(), + headers: &client_headers, + operation: OperationDetails { + name: None, + query: "{ __typename }".to_string().into(), + kind: "query", + }, + }; + + let mut out = HeaderMap::new(); + modify_subgraph_request_headers(&plan, "any", &client_details, &mut out).unwrap(); + + insta::assert_snapshot!(out.to_string(), @r#" + x-keep: hi + "#); + } +} diff --git a/lib/executor/src/headers/plan.rs b/lib/executor/src/headers/plan.rs new file mode 100644 index 000000000..a1a3cf990 --- /dev/null +++ b/lib/executor/src/headers/plan.rs @@ -0,0 +1,132 @@ +use ahash::HashMap; +use http::{HeaderName, HeaderValue}; +use regex_automata::meta::Regex; +use vrl::compiler::Program as VrlProgram; + +#[derive(Clone)] +pub struct HeaderRulesPlan { + pub request: RequestHeaderRules, + pub response: ResponseHeaderRules, +} + +type SubgraphName = String; + +#[derive(Clone, Default)] +pub struct RequestHeaderRules { + pub global: Vec, + pub by_subgraph: HashMap>, +} + +#[derive(Clone, Default)] +pub struct ResponseHeaderRules { + pub global: Vec, + pub by_subgraph: HashMap>, +} + +#[derive(Clone)] +pub enum RequestHeaderRule { + PropagateNamed(RequestPropagateNamed), + PropagateRegex(RequestPropagateRegex), + InsertStatic(RequestInsertStatic), + InsertExpression(RequestInsertExpression), + RemoveNamed(RequestRemoveNamed), + RemoveRegex(RequestRemoveRegex), +} + +#[derive(Clone)] +pub struct RequestPropagateNamed { + pub names: Vec, + pub default: Option, + pub rename: Option, +} + +#[derive(Clone)] +pub struct RequestPropagateRegex { + pub include: Option, + pub exclude: Option, +} + +#[derive(Clone)] +pub struct RequestInsertStatic { + pub name: HeaderName, + pub value: HeaderValue, +} + +#[derive(Clone)] +pub struct ResponseInsertStatic { + pub name: HeaderName, + pub value: HeaderValue, + pub strategy: HeaderAggregationStrategy, +} + +#[derive(Clone)] +pub struct RequestInsertExpression { + pub name: HeaderName, + pub expression: Box, +} + +#[derive(Clone)] +pub struct ResponseInsertExpression { + pub name: HeaderName, + pub expression: Box, + pub strategy: HeaderAggregationStrategy, +} + +#[derive(Clone)] +pub struct RequestRemoveNamed { + pub names: Vec, +} + +#[derive(Clone)] +pub struct ResponseRemoveNamed { + pub names: Vec, +} + +#[derive(Clone)] +pub struct RequestRemoveRegex { + pub regex: Regex, +} + +#[derive(Clone)] +pub struct ResponseRemoveRegex { + pub regex: Regex, +} + +#[derive(Clone)] +pub enum ResponseHeaderRule { + PropagateNamed(ResponsePropagateNamed), + PropagateRegex(ResponsePropagateRegex), + InsertStatic(ResponseInsertStatic), + InsertExpression(ResponseInsertExpression), + RemoveNamed(ResponseRemoveNamed), + RemoveRegex(ResponseRemoveRegex), +} + +#[derive(Clone)] +pub struct ResponsePropagateNamed { + pub names: Vec, + pub rename: Option, + pub default: Option, + pub strategy: HeaderAggregationStrategy, +} + +#[derive(Clone)] +pub struct ResponsePropagateRegex { + pub include: Option, + pub exclude: Option, + pub strategy: HeaderAggregationStrategy, +} + +#[derive(Clone, Copy)] +pub enum HeaderAggregationStrategy { + First, + Last, + Append, +} + +type AggregatedHeader = (HeaderAggregationStrategy, Vec); + +#[derive(Default)] +pub struct ResponseHeaderAggregator { + pub entries: HashMap, +} diff --git a/lib/executor/src/headers/request.rs b/lib/executor/src/headers/request.rs new file mode 100644 index 000000000..ebc912448 --- /dev/null +++ b/lib/executor/src/headers/request.rs @@ -0,0 +1,243 @@ +use std::collections::BTreeMap; + +use http::HeaderMap; +use vrl::{ + compiler::TargetValue as VrlTargetValue, + core::Value as VrlValue, + prelude::{state::RuntimeState as VrlState, Context as VrlContext, TimeZone as VrlTimeZone}, + value::Secrets as VrlSecrets, +}; + +use crate::{ + execution::plan::ClientRequestDetails, + headers::{ + errors::HeaderRuleRuntimeError, + expression::vrl_value_to_header_value, + plan::{ + HeaderRulesPlan, RequestHeaderRule, RequestInsertExpression, RequestInsertStatic, + RequestPropagateNamed, RequestPropagateRegex, RequestRemoveNamed, RequestRemoveRegex, + }, + sanitizer::{is_denied_header, is_never_join_header}, + }, +}; + +pub fn modify_subgraph_request_headers( + header_rule_plan: &HeaderRulesPlan, + subgraph_name: &str, + client_request: &ClientRequestDetails, + output_headers: &mut HeaderMap, +) -> Result<(), HeaderRuleRuntimeError> { + let global_actions = &header_rule_plan.request.global; + let subgraph_actions = header_rule_plan.request.by_subgraph.get(subgraph_name); + + let ctx = RequestExpressionContext { + subgraph_name, + client_request, + }; + + for action in global_actions + .iter() + .chain(subgraph_actions.into_iter().flatten()) + { + action.apply_request_headers(&ctx, output_headers)?; + } + + Ok(()) +} + +pub struct RequestExpressionContext<'a> { + pub subgraph_name: &'a str, + pub client_request: &'a ClientRequestDetails<'a>, +} + +trait ApplyRequestHeader { + fn apply_request_headers( + &self, + ctx: &RequestExpressionContext, + output_headers: &mut HeaderMap, + ) -> Result<(), HeaderRuleRuntimeError>; +} + +impl ApplyRequestHeader for RequestHeaderRule { + fn apply_request_headers( + &self, + ctx: &RequestExpressionContext, + output_headers: &mut HeaderMap, + ) -> Result<(), HeaderRuleRuntimeError> { + match self { + Self::PropagateNamed(data) => data.apply_request_headers(ctx, output_headers), + Self::PropagateRegex(data) => data.apply_request_headers(ctx, output_headers), + Self::InsertStatic(data) => data.apply_request_headers(ctx, output_headers), + Self::InsertExpression(data) => data.apply_request_headers(ctx, output_headers), + Self::RemoveNamed(data) => data.apply_request_headers(ctx, output_headers), + Self::RemoveRegex(data) => data.apply_request_headers(ctx, output_headers), + } + } +} + +impl ApplyRequestHeader for RequestPropagateNamed { + fn apply_request_headers( + &self, + ctx: &RequestExpressionContext, + output_headers: &mut HeaderMap, + ) -> Result<(), HeaderRuleRuntimeError> { + let mut matched = false; + + for header_name in &self.names { + if is_denied_header(header_name) { + continue; + } + if let Some(header_value) = ctx.client_request.headers.get(header_name) { + let destination_name = self.rename.as_ref().unwrap_or(header_name); + output_headers.append(destination_name, header_value.into()); + matched = true; + } + } + + if !matched { + // If no headers matched, and a default is provided, use it + if let (Some(default_value), Some(first_name)) = (&self.default, self.names.first()) { + let destination_name = self.rename.as_ref().unwrap_or(first_name); + + if is_denied_header(destination_name) { + return Ok(()); + } + + output_headers.append(destination_name, default_value.clone()); + } + } + + Ok(()) + } +} + +impl ApplyRequestHeader for RequestPropagateRegex { + fn apply_request_headers( + &self, + ctx: &RequestExpressionContext, + output_headers: &mut HeaderMap, + ) -> Result<(), HeaderRuleRuntimeError> { + for (header_name, header_value) in ctx.client_request.headers { + if is_denied_header(header_name) { + continue; + } + + let header_bytes = header_name.as_str().as_bytes(); + + let Some(include_regex) = &self.include else { + continue; + }; + + if !include_regex.is_match(header_bytes) { + continue; + } + + if self + .exclude + .as_ref() + .is_some_and(|regex| regex.is_match(header_bytes)) + { + continue; + } + + output_headers.append(header_name, header_value.into()); + } + + Ok(()) + } +} + +impl ApplyRequestHeader for RequestInsertStatic { + fn apply_request_headers( + &self, + _ctx: &RequestExpressionContext, + output_headers: &mut HeaderMap, + ) -> Result<(), HeaderRuleRuntimeError> { + if !is_denied_header(&self.name) { + if is_never_join_header(&self.name) { + output_headers.append(self.name.clone(), self.value.clone()); + } else { + output_headers.insert(self.name.clone(), self.value.clone()); + } + } + + Ok(()) + } +} + +impl ApplyRequestHeader for RequestInsertExpression { + fn apply_request_headers( + &self, + ctx: &RequestExpressionContext, + output_headers: &mut HeaderMap, + ) -> Result<(), HeaderRuleRuntimeError> { + if is_denied_header(&self.name) { + return Ok(()); + } + + let mut target = VrlTargetValue { + value: ctx.into(), + metadata: VrlValue::Object(BTreeMap::new()), + secrets: VrlSecrets::default(), + }; + + let mut state = VrlState::default(); + let timezone = VrlTimeZone::default(); + let mut ctx = VrlContext::new(&mut target, &mut state, &timezone); + let value = self.expression.resolve(&mut ctx).map_err(|err| { + HeaderRuleRuntimeError::new_expression_evaluation(self.name.to_string(), Box::new(err)) + })?; + + if let Some(header_value) = vrl_value_to_header_value(value) { + if is_never_join_header(&self.name) { + output_headers.append(self.name.clone(), header_value); + } else { + output_headers.insert(self.name.clone(), header_value); + } + } + + Ok(()) + } +} + +impl ApplyRequestHeader for RequestRemoveNamed { + fn apply_request_headers( + &self, + _ctx: &RequestExpressionContext, + output_headers: &mut HeaderMap, + ) -> Result<(), HeaderRuleRuntimeError> { + for header_name in &self.names { + if is_denied_header(header_name) { + continue; + } + output_headers.remove(header_name); + } + + Ok(()) + } +} + +impl ApplyRequestHeader for RequestRemoveRegex { + fn apply_request_headers( + &self, + _ctx: &RequestExpressionContext, + output_headers: &mut HeaderMap, + ) -> Result<(), HeaderRuleRuntimeError> { + let mut headers_to_remove = Vec::new(); + for header_name in output_headers.keys() { + if is_denied_header(header_name) { + continue; + } + + if self.regex.is_match(header_name.as_str().as_bytes()) { + headers_to_remove.push(header_name.clone()); + } + } + + for header_name in headers_to_remove.iter() { + output_headers.remove(header_name); + } + + Ok(()) + } +} diff --git a/lib/executor/src/headers/response.rs b/lib/executor/src/headers/response.rs new file mode 100644 index 000000000..1de5ccdf0 --- /dev/null +++ b/lib/executor/src/headers/response.rs @@ -0,0 +1,353 @@ +use std::{collections::BTreeMap, iter::once}; + +use crate::{ + execution::plan::ClientRequestDetails, + headers::{ + errors::HeaderRuleRuntimeError, + expression::vrl_value_to_header_value, + plan::{ + HeaderAggregationStrategy, HeaderRulesPlan, ResponseHeaderAggregator, + ResponseHeaderRule, ResponseInsertExpression, ResponseInsertStatic, + ResponsePropagateNamed, ResponsePropagateRegex, ResponseRemoveNamed, + ResponseRemoveRegex, + }, + sanitizer::is_denied_header, + }, +}; + +use super::sanitizer::is_never_join_header; +use http::{header::InvalidHeaderValue, HeaderMap, HeaderName, HeaderValue}; +use vrl::{ + compiler::TargetValue as VrlTargetValue, + core::Value as VrlValue, + prelude::{state::RuntimeState as VrlState, Context as VrlContext, TimeZone as VrlTimeZone}, + value::Secrets as VrlSecrets, +}; + +pub fn apply_subgraph_response_headers( + header_rule_plan: &HeaderRulesPlan, + subgraph_name: &str, + subgraph_headers: &HeaderMap, + client_request_details: &ClientRequestDetails, + accumulator: &mut ResponseHeaderAggregator, +) -> Result<(), HeaderRuleRuntimeError> { + let global_actions = &header_rule_plan.response.global; + let subgraph_actions = header_rule_plan.response.by_subgraph.get(subgraph_name); + + let ctx = ResponseExpressionContext { + subgraph_name, + subgraph_headers, + client_request: client_request_details, + }; + + for action in global_actions + .iter() + .chain(subgraph_actions.into_iter().flatten()) + { + action.apply_response_headers(&ctx, accumulator)?; + } + + Ok(()) +} + +pub struct ResponseExpressionContext<'a> { + pub subgraph_name: &'a str, + pub client_request: &'a ClientRequestDetails<'a>, + pub subgraph_headers: &'a HeaderMap, +} + +trait ApplyResponseHeader { + fn apply_response_headers( + &self, + ctx: &ResponseExpressionContext, + accumulator: &mut ResponseHeaderAggregator, + ) -> Result<(), HeaderRuleRuntimeError>; +} + +impl ApplyResponseHeader for ResponseHeaderRule { + fn apply_response_headers( + &self, + ctx: &ResponseExpressionContext, + accumulator: &mut ResponseHeaderAggregator, + ) -> Result<(), HeaderRuleRuntimeError> { + match self { + ResponseHeaderRule::PropagateNamed(data) => { + data.apply_response_headers(ctx, accumulator) + } + ResponseHeaderRule::PropagateRegex(data) => { + data.apply_response_headers(ctx, accumulator) + } + ResponseHeaderRule::InsertStatic(data) => data.apply_response_headers(ctx, accumulator), + ResponseHeaderRule::InsertExpression(data) => { + data.apply_response_headers(ctx, accumulator) + } + ResponseHeaderRule::RemoveNamed(data) => data.apply_response_headers(ctx, accumulator), + ResponseHeaderRule::RemoveRegex(data) => data.apply_response_headers(ctx, accumulator), + } + } +} + +impl ApplyResponseHeader for ResponsePropagateNamed { + fn apply_response_headers( + &self, + ctx: &ResponseExpressionContext, + accumulator: &mut ResponseHeaderAggregator, + ) -> Result<(), HeaderRuleRuntimeError> { + let mut matched = false; + + for header_name in &self.names { + if is_denied_header(header_name) { + continue; + } + + if let Some(header_value) = ctx.subgraph_headers.get(header_name) { + matched = true; + write_agg( + accumulator, + self.rename.as_ref().unwrap_or(header_name), + header_value, + self.strategy, + ); + } + } + + if !matched { + if let (Some(default_value), Some(first_name)) = (&self.default, self.names.first()) { + let destination_name = self.rename.as_ref().unwrap_or(first_name); + + if is_denied_header(destination_name) { + return Ok(()); + } + + write_agg(accumulator, destination_name, default_value, self.strategy); + } + } + + Ok(()) + } +} + +impl ApplyResponseHeader for ResponsePropagateRegex { + fn apply_response_headers( + &self, + ctx: &ResponseExpressionContext, + accumulator: &mut ResponseHeaderAggregator, + ) -> Result<(), HeaderRuleRuntimeError> { + for (header_name, header_value) in ctx.subgraph_headers { + if is_denied_header(header_name) { + continue; + } + + let header_bytes = header_name.as_str().as_bytes(); + + let Some(include_regex) = &self.include else { + continue; + }; + + if !include_regex.is_match(header_bytes) { + continue; + } + + if self + .exclude + .as_ref() + .is_some_and(|regex| regex.is_match(header_bytes)) + { + continue; + } + + write_agg(accumulator, header_name, header_value, self.strategy); + } + + Ok(()) + } +} + +impl ApplyResponseHeader for ResponseInsertStatic { + fn apply_response_headers( + &self, + _ctx: &ResponseExpressionContext, + accumulator: &mut ResponseHeaderAggregator, + ) -> Result<(), HeaderRuleRuntimeError> { + if is_denied_header(&self.name) { + return Ok(()); + } + + let strategy = if is_never_join_header(&self.name) { + HeaderAggregationStrategy::Append + } else { + self.strategy + }; + + write_agg(accumulator, &self.name, &self.value, strategy); + + Ok(()) + } +} + +impl ApplyResponseHeader for ResponseInsertExpression { + fn apply_response_headers( + &self, + ctx: &ResponseExpressionContext, + accumulator: &mut ResponseHeaderAggregator, + ) -> Result<(), HeaderRuleRuntimeError> { + if is_denied_header(&self.name) { + return Ok(()); + } + + let mut target = VrlTargetValue { + value: ctx.into(), + metadata: VrlValue::Object(BTreeMap::new()), + secrets: VrlSecrets::default(), + }; + + let mut state = VrlState::default(); + let timezone = VrlTimeZone::default(); + let mut ctx = VrlContext::new(&mut target, &mut state, &timezone); + let value = self.expression.resolve(&mut ctx).map_err(|err| { + HeaderRuleRuntimeError::ExpressionEvaluation(self.name.to_string(), Box::new(err)) + })?; + + if let Some(header_value) = vrl_value_to_header_value(value) { + let strategy = if is_never_join_header(&self.name) { + HeaderAggregationStrategy::Append + } else { + self.strategy + }; + + write_agg(accumulator, &self.name, &header_value, strategy); + } + + Ok(()) + } +} + +impl ApplyResponseHeader for ResponseRemoveNamed { + fn apply_response_headers( + &self, + _ctx: &ResponseExpressionContext, + accumulator: &mut ResponseHeaderAggregator, + ) -> Result<(), HeaderRuleRuntimeError> { + for header_name in &self.names { + if is_denied_header(header_name) { + continue; + } + accumulator.entries.remove(header_name); + } + + Ok(()) + } +} + +impl ApplyResponseHeader for ResponseRemoveRegex { + fn apply_response_headers( + &self, + _ctx: &ResponseExpressionContext, + accumulator: &mut ResponseHeaderAggregator, + ) -> Result<(), HeaderRuleRuntimeError> { + accumulator.entries.retain(|name, _| { + if is_denied_header(name) { + // Denied headers (hop-by–hop) are never inserted in the first place + // and should not be removed here. + return true; + } + + !self.regex.is_match(name.as_str().as_bytes()) + }); + + Ok(()) + } +} + +/// Write a header to the aggregator according to the specified strategy. +fn write_agg( + agg: &mut ResponseHeaderAggregator, + name: &HeaderName, + value: &HeaderValue, + strategy: HeaderAggregationStrategy, +) { + let strategy = if is_never_join_header(name) { + HeaderAggregationStrategy::Append + } else { + strategy + }; + + if !agg.entries.contains_key(name) { + agg.entries + .insert(name.clone(), (strategy, once(value.clone()).collect())); + return; + } + + // The `expect` is safe because we just inserted the entry if it didn't exist + let (strategy, values) = agg.entries.get_mut(name).expect("Expected entry to exist"); + + match (strategy, values.len()) { + (HeaderAggregationStrategy::First, 0) => { + values.push(value.clone()); + } + (HeaderAggregationStrategy::Last, _) => { + values.clear(); + values.push(value.clone()); + } + (HeaderAggregationStrategy::Append, _) => { + values.push(value.clone()); + } + (_, _) => {} + } +} + +/// Modify the outgoing client response headers based on the aggregated headers from subgraphs. +pub fn modify_client_response_headers( + agg: ResponseHeaderAggregator, + out: &mut HeaderMap, +) -> Result<(), HeaderRuleRuntimeError> { + for (name, (agg_strategy, mut values)) in agg.entries { + if values.is_empty() { + continue; + } + + if is_never_join_header(&name) { + // never-join headers must be emitted as multiple header fields + for value in values { + out.append(name.clone(), value); + } + continue; + } + + if values.len() == 1 { + out.insert(name, values.pop().unwrap()); + continue; + } + + if matches!(agg_strategy, HeaderAggregationStrategy::Append) { + let joined = join_with_comma(&values) + .map_err(|_| HeaderRuleRuntimeError::BadHeaderValue(name.to_string()))?; + out.insert(name, joined); + } + } + + Ok(()) +} + +#[inline] +fn join_with_comma(values: &[HeaderValue]) -> Result { + // Compute capacity: sum of lengths + ", ".len() * (n-1) + let mut cap = 0usize; + + for value in values { + cap += value.as_bytes().len(); + } + + if values.len() > 1 { + cap += 2 * (values.len() - 1); + } + + let mut buf = Vec::with_capacity(cap); + for (idx, value) in values.iter().enumerate() { + if idx > 0 { + buf.extend_from_slice(b", "); + } + buf.extend_from_slice(value.as_bytes()); + } + HeaderValue::from_bytes(&buf) +} diff --git a/lib/executor/src/headers/sanitizer.rs b/lib/executor/src/headers/sanitizer.rs new file mode 100644 index 000000000..1220d524c --- /dev/null +++ b/lib/executor/src/headers/sanitizer.rs @@ -0,0 +1,11 @@ +use hive_router_config::headers::{HOP_BY_HOP_HEADERS, NEVER_JOIN_HEADERS}; +use http::HeaderName; + +#[inline] +pub fn is_denied_header(name: &http::HeaderName) -> bool { + HOP_BY_HOP_HEADERS.contains(&name.as_str()) +} + +pub fn is_never_join_header(name: &HeaderName) -> bool { + NEVER_JOIN_HEADERS.contains(&name.as_str()) +} diff --git a/lib/executor/src/lib.rs b/lib/executor/src/lib.rs index 247fb0778..4f912a463 100644 --- a/lib/executor/src/lib.rs +++ b/lib/executor/src/lib.rs @@ -1,6 +1,7 @@ pub mod context; pub mod execution; pub mod executors; +pub mod headers; pub mod introspection; pub mod json_writer; pub mod projection; diff --git a/lib/router-config/Cargo.toml b/lib/router-config/Cargo.toml index 21440e07f..5fa599321 100644 --- a/lib/router-config/Cargo.toml +++ b/lib/router-config/Cargo.toml @@ -19,6 +19,7 @@ path = "src/config_schema_generator.rs" serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } +http = { workspace = true } schemars = "1.0.4" humantime-serde = "1.1.1" config = { version = "0.15.14", features = ["yaml", "json", "json5"] } diff --git a/lib/router-config/src/headers.rs b/lib/router-config/src/headers.rs new file mode 100644 index 000000000..676e157a9 --- /dev/null +++ b/lib/router-config/src/headers.rs @@ -0,0 +1,384 @@ +use std::collections::HashMap; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +type HeaderName = String; +type RegExp = String; + +/// Standard hop-by-hop headers that are never forwarded to subgraphs and are +/// filtered from client responses, regardless of rules. +pub const HOP_BY_HOP_HEADERS: &[&str] = &[ + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade", + "proxy-connection", + "host", + "content-length", +]; + +/// Headers that must never be comma-joined. If multiple values exist, they +/// are emitted as separate header fields (e.g. multiple `Set-Cookie` lines). +pub static NEVER_JOIN_HEADERS: &[&str] = &["set-cookie", "www-authenticate"]; + +/// Configuration for how the Router handles HTTP headers. +/// +/// ## Scopes & order of evaluation +/// - **Scope precedence:** Rules under `all` apply to every subgraph first. +/// Rules under `subgraphs.` apply **after** and can override results +/// for that specific subgraph. +/// - **Rule ordering:** Within each list, rules are applied **top-to-bottom**. +/// Later rules can overwrite/undo earlier rules (e.g. `propagate` then `remove`). +/// +/// ## Case-insensitive names +/// Header names are case-insensitive. Internally they are normalized to lowercase. +/// +/// ## Safety +/// Hop-by-hop headers are always stripped. Never-join headers (e.g. `set-cookie`) +/// are never comma-joined. Multiple values are preserved as separate fields. +/// +/// ### Example +/// ```yaml +/// headers: +/// all: +/// request: +/// - propagate: +/// named: Authorization +/// - remove: +/// matching: "^x-legacy-.*" +/// - insert: +/// name: x-router +/// value: hive-router +/// +/// subgraphs: +/// accounts: +/// request: +/// - propagate: +/// named: x-tenant-id +/// rename: x-acct-tenant +/// default: unknown +/// ``` +#[derive(Debug, Default, Deserialize, Serialize, JsonSchema, Clone)] +pub struct HeadersConfig { + /// Rules applied to all subgraphs (global defaults). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub all: Option, + + /// Rules applied to individual subgraphs. + /// Keys are subgraph names as defined in the supergraph schema. + /// + /// **Precedence:** These are applied **after** `all`, and therefore can + /// override the result of global rules for that subgraph. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub subgraphs: Option>, +} + +/// Rules for a single scope (global or per-subgraph). +/// +/// You can specify independent rule lists for **request** (to subgraphs) +/// and **response** (to clients). Within each list, rules are applied in order. +#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone, Default)] +pub struct HeaderRules { + /// Rules that shape the **request** sent from the router to subgraphs. + #[serde(default)] + pub request: Option>, + + /// Rules that shape the **response** sent from the router back to the client. + #[serde(default)] + pub response: Option>, +} + +/// Request-header rules (applied before sending to a subgraph). +#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)] +#[serde(rename_all = "snake_case")] +pub enum RequestHeaderRule { + /// Forward headers from the client request into the subgraph request. + /// + /// - If `rename` is set, the header is forwarded under the new name. + /// - If **none** of the matched headers exist, `default` is used (when provided). + /// + /// **Order matters:** You can propagate first and then `remove` or `insert` + /// to refine the final output. + Propagate(RequestPropagateRule), + + /// Remove headers before sending the request to a subgraph. + /// + /// Useful to drop sensitive or irrelevant headers, or to undo a previous + /// `propagate`/`insert`. + Remove(RemoveRule), + + /// Add or overwrite a header with a static value. + /// + /// - For **normal** headers: replaces any existing value. + /// - For **never-join** headers (e.g. `set-cookie`): **appends** another + /// occurrence (multiple lines), never comma-joins. + Insert(RequestInsertRule), +} + +/// Response-header rules (applied before sending back to the client). +#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)] +#[serde(rename_all = "snake_case")] +pub enum ResponseHeaderRule { + /// Forward headers from subgraph responses into the final client response. + /// + /// - If multiple subgraphs provide the same header, `algorithm` controls + /// how values are merged. + /// - If **no** subgraph provides a matching header, `default` is used (when provided). + /// - If `rename` is set, the header is returned under the new name. + /// + /// **Never-join headers** (e.g. `set-cookie`) are never comma-joined: + /// multiple values are returned as separate header fields regardless of `algorithm`. + Propagate(ResponsePropagateRule), + + /// Remove headers before sending the response to the client. + Remove(RemoveRule), + + /// Add or overwrite a header in the response to the client. + /// + /// For never-join headers, appends another occurrence (multiple lines). + Insert(ResponseInsertRule), +} + +/// Remove headers matched by the specification. +#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)] +pub struct RemoveRule { + #[serde(flatten)] + pub spec: MatchSpec, +} + +/// Insert a header with a static value. +/// +/// ### Examples +/// ```yaml +/// - insert: +/// name: x-env +/// value: prod +/// ``` +/// +/// ```yaml +/// - insert: +/// name: set-cookie +/// value: "a=1; Path=/" +/// # If another Set-Cookie exists, this creates another header line (never joined) +/// ``` +#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)] +pub struct RequestInsertRule { + /// Header name to insert or overwrite (case-insensitive). + pub name: HeaderName, + /// Where the value comes from (currently static only). + #[serde(flatten)] + pub source: InsertSource, +} + +/// Insert a header with a static value. +/// +/// ### Examples +/// ```yaml +/// - insert: +/// name: x-env +/// value: prod +/// ``` +/// +/// ```yaml +/// - insert: +/// name: set-cookie +/// value: "a=1; Path=/" +/// # If another Set-Cookie exists, this creates another header line (never joined) +/// ``` +#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)] +pub struct ResponseInsertRule { + /// Header name to insert or overwrite (case-insensitive). + pub name: HeaderName, + /// Where the value comes from (currently static only). + #[serde(flatten)] + pub source: InsertSource, + /// How to merge values across multiple subgraph responses. + /// Default: `Last` (overwrite). + #[serde(default)] + pub algorithm: Option, +} + +/// Source for an inserted header value. +#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)] +#[serde(untagged)] +pub enum InsertSource { + /// Static value provided in the config. + Value { value: String }, + /// A dynamic value computed by a VRL expression. + /// + /// This allows you to generate header values based on the incoming request, + /// subgraph name, and (for response rules) subgraph response headers. + /// The expression has access to a context object with `.request`, `.subgraph`, + /// and `.response` fields. + /// + /// For more information on the available functions and syntax, see the + /// [VRL documentation](https://vrl.dev/). + /// + /// ### Example + /// ```yaml + /// # Insert a header with a value derived from another header. + /// - insert: + /// name: x-auth-scheme + /// expression: 'split(.request.headers.authorization, " ")[0] ?? "none"' + /// ``` + Expression { expression: String }, +} + +/// Helper to allow `one` or `many` values for ergonomics (OR semantics). +/// +/// ### Examples +/// ```yaml +/// named: Authorization +/// # or +/// named: [Authorization, x-tenant-id] +/// ``` +#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)] +#[serde(untagged)] +pub enum OneOrMany { + One(T), + Many(Vec), +} + +/// Header matching specification used by `propagate` and `remove`. +/// +/// **Semantics** +/// - `named`: match by exact name(s), case-insensitive (OR). +/// - `matching`: match header name(s) by regex (OR). +/// - `exclude`: subtract matches by regex (applied **after** `named`/`matching`). +/// +/// If `matching` is omitted, it’s treated as “match nothing” unless `named` is set. +/// If both `named` and `matching` are omitted, the rule matches nothing. +/// +/// **Safety:** Hop-by-hop headers are never propagated, even if matched here. +/// +/// ### Examples +/// ```yaml +/// # Propagate selected exact names +/// named: [Authorization, x-corr-id] +/// +/// # Propagate everything starting with x- (except legacy) +/// matching: "^x-.*" +/// exclude: ["^x-legacy-.*"] +/// ``` +#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone, Default)] +pub struct MatchSpec { + /// Match headers by exact name (OR). + #[serde(default)] + pub named: Option>, + + /// Match headers by regex pattern(s) (OR). + #[serde(default)] + pub matching: Option>, + + /// Exclude headers matching these regexes, applied after `matching`. + #[serde(default)] + pub exclude: Option>, +} + +/// Propagate headers from the client request to subgraph requests. +/// +/// **Behavior** +/// - If `rename` is provided, forwarded under that name. +/// - If **none** of the matched headers are present, `default` (when present) +/// is used under `rename` (if set) or the **first** `named` header. +/// +/// ### Examples +/// ```yaml +/// # Forward a specific header, but rename it per subgraph +/// propagate: +/// named: x-tenant-id +/// rename: x-acct-tenant +/// +/// # Forward all x- headers except legacy ones +/// propagate: +/// matching: "^x-.*" +/// exclude: ["^x-legacy-.*"] +/// +/// # If Authorization is missing, inject a default token for this subgraph +/// propagate: +/// named: Authorization +/// default: "Bearer test-token" +/// ``` +#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)] +pub struct RequestPropagateRule { + #[serde(flatten)] + pub spec: MatchSpec, + + /// Optionally rename the header when forwarding. + #[serde(default)] + pub rename: Option, + + /// If the header is missing, set a default value. + /// Applied only when **none** of the matched headers exist. + + #[serde(default)] + pub default: Option, +} + +/// How to merge response header values from multiple subgraphs. +/// +/// - `First`: keep the first value encountered, ignore the rest. +/// - `Last`: overwrite with the last value encountered (**default**). +/// - `Append`: comma-join all values into a single field, **except** for +/// `NEVER_JOIN_HEADERS` which are always emitted as multiple fields. +/// +/// **Note:** For never-join headers (e.g. `Set-Cookie`), the router always +/// emits multiple header fields, regardless of the algorithm. +#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone, Copy)] +#[serde(rename_all = "snake_case")] +pub enum AggregationAlgo { + /// Take the first value encountered and ignore later ones. + First, + /// Overwrite with the last value encountered. + Last, + /// Append all values into a comma-separated string (list-valued headers). + Append, +} + +/// Propagate headers from subgraph responses to the final client response. +/// +/// **Behavior** +/// - If multiple subgraphs return the header, values are merged using `algorithm`. +/// Never-join headers are **never** comma-joined. +/// - If **no** subgraph returns a match, `default` (if set) is emitted. +/// - If `rename` is set, the outgoing header uses the new name. +/// +/// ### Examples +/// ```yaml +/// # Forward Cache-Control from whichever subgraph supplies it (last wins) +/// propagate: +/// named: Cache-Control +/// algorithm: last +/// +/// # Combine list-valued headers +/// propagate: +/// named: vary +/// algorithm: append +/// +/// # Ensure a fallback header is always present +/// propagate: +/// named: x-backend +/// algorithm: append +/// default: unknown +/// ``` +#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)] +pub struct ResponsePropagateRule { + #[serde(flatten)] + pub spec: MatchSpec, + + /// Optionally rename the header when returning it to the client. + #[serde(default)] + pub rename: Option, + + /// If no subgraph returns the header, set this default value. + #[serde(default)] + pub default: Option, + + /// How to merge values across multiple subgraph responses. + pub algorithm: AggregationAlgo, +} diff --git a/lib/router-config/src/lib.rs b/lib/router-config/src/lib.rs index 92a418ba2..646f38e61 100644 --- a/lib/router-config/src/lib.rs +++ b/lib/router-config/src/lib.rs @@ -1,3 +1,4 @@ +pub mod headers; pub mod http_server; pub mod log; pub mod primitives; @@ -5,7 +6,7 @@ pub mod query_planner; pub mod supergraph; pub mod traffic_shaping; -use config::{Config, Environment, File, FileSourceFile}; +use config::{Config, Environment, File, FileFormat, FileSourceFile}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -40,6 +41,10 @@ pub struct HiveRouterConfig { /// Configuration for the traffic-shaper executor. Use these configurations to control how requests are being executed to subgraphs. #[serde(default)] pub traffic_shaping: TrafficShapingExecutorConfig, + + /// Configuration for the headers. + #[serde(default)] + pub headers: headers::HeadersConfig, } #[derive(Debug, thiserror::Error)] @@ -77,3 +82,10 @@ pub fn load_config( .build()? .try_deserialize::() } + +pub fn parse_yaml_config(config_raw: String) -> Result { + Config::builder() + .add_source(File::from_str(&config_raw, FileFormat::Yaml)) + .build()? + .try_deserialize::() +} diff --git a/lib/router-config/src/traffic_shaping.rs b/lib/router-config/src/traffic_shaping.rs index eec853fda..595112e37 100644 --- a/lib/router-config/src/traffic_shaping.rs +++ b/lib/router-config/src/traffic_shaping.rs @@ -17,12 +17,6 @@ pub struct TrafficShapingExecutorConfig { /// be deduplicated by sharing the response of other in-flight requests. #[serde(default = "default_dedupe_enabled")] pub dedupe_enabled: bool, - - /// A list of headers that should be used to fingerprint requests for deduplication. - /// - /// If not provided, the default is to use the "authorization" header only. - #[serde(default = "default_dedupe_fingerprint_headers")] - pub dedupe_fingerprint_headers: Vec, } impl Default for TrafficShapingExecutorConfig { @@ -31,7 +25,6 @@ impl Default for TrafficShapingExecutorConfig { max_connections_per_host: default_max_connections_per_host(), pool_idle_timeout_seconds: default_pool_idle_timeout_seconds(), dedupe_enabled: default_dedupe_enabled(), - dedupe_fingerprint_headers: default_dedupe_fingerprint_headers(), } } } @@ -47,7 +40,3 @@ fn default_pool_idle_timeout_seconds() -> u64 { fn default_dedupe_enabled() -> bool { true } - -fn default_dedupe_fingerprint_headers() -> Vec { - vec!["authorization".to_string()] -} From 8b877ab395e861347ad90b0fd1f26012608b11bc Mon Sep 17 00:00:00 2001 From: Dotan Simha Date: Mon, 6 Oct 2025 10:40:10 +0300 Subject: [PATCH 2/2] fix some config defaults --- docs/README.md | 280 ++++++------------------------- lib/router-config/src/headers.rs | 89 ++++++---- 2 files changed, 104 insertions(+), 265 deletions(-) diff --git a/docs/README.md b/docs/README.md index 22033bbca..da6009916 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,7 +15,23 @@ **Example** ```yaml -headers: {} +headers: + all: + request: + - propagate: + named: Authorization + - remove: + matching: ^x-legacy-.* + - insert: + name: x-router + value: hive-router + subgraphs: + accounts: + request: + - propagate: + default: unknown + named: x-tenant-id + rename: x-acct-tenant http: host: 0.0.0.0 port: 4000 @@ -49,6 +65,28 @@ Configuration for the headers. |[**all**](#headersall)|`object`, `null`|Rules applied to all subgraphs (global defaults).
|| |[**subgraphs**](#headerssubgraphs)|`object`, `null`|Rules applied to individual subgraphs.
|| +**Example** + +```yaml +all: + request: + - propagate: + named: Authorization + - remove: + matching: ^x-legacy-.* + - insert: + name: x-router + value: hive-router +subgraphs: + accounts: + request: + - propagate: + default: unknown + named: x-tenant-id + rename: x-acct-tenant + +``` +
### headers\.all: object,null @@ -94,12 +132,7 @@ to refine the final output. **Example** ```yaml -propagate: - default: null - exclude: null - matching: null - named: null - rename: null +propagate: {} ``` @@ -122,10 +155,7 @@ Useful to drop sensitive or irrelevant headers, or to undo a previous **Example** ```yaml -remove: - exclude: null - matching: null - named: null +remove: {} ``` @@ -154,13 +184,6 @@ insert: {} ``` -**Example** - -```yaml -{} - -``` - ## Option 1: propagate: object @@ -200,17 +223,6 @@ propagate: |**named**||Match headers by exact name (OR).
|| |**rename**|`string`, `null`|Optionally rename the header when forwarding.
|| -**Example** - -```yaml -default: null -exclude: null -matching: null -named: null -rename: null - -``` - ### Option 1: propagate\.exclude\[\]: array,null @@ -220,13 +232,6 @@ Exclude headers matching these regexes, applied after `matching`. **Items** **Item Type:** `string` -**Example** - -```yaml -{} - -``` - ## Option 2: remove: object @@ -241,15 +246,6 @@ Remove headers matched by the specification. |**matching**||Match headers by regex pattern(s) (OR).
|| |**named**||Match headers by exact name (OR).
|| -**Example** - -```yaml -exclude: null -matching: null -named: null - -``` - ### Option 2: remove\.exclude\[\]: array,null @@ -259,13 +255,6 @@ Exclude headers matching these regexes, applied after `matching`. **Items** **Item Type:** `string` -**Example** - -```yaml -{} - -``` - ## Option 3: insert: object @@ -366,12 +355,7 @@ multiple values are returned as separate header fields regardless of `algorithm` **Example** ```yaml -propagate: - default: null - exclude: null - matching: null - named: null - rename: null +propagate: {} ``` @@ -391,10 +375,7 @@ Remove headers before sending the response to the client. **Example** ```yaml -remove: - exclude: null - matching: null - named: null +remove: {} ``` @@ -416,19 +397,11 @@ For never-join headers, appends another occurrence (multiple lines). **Example** ```yaml -insert: - algorithm: null +insert: {} ``` -**Example** - -```yaml -{} - -``` - ## Option 1: propagate: object @@ -471,17 +444,6 @@ propagate: |**named**||Match headers by exact name (OR).
|no| |**rename**|`string`, `null`|Optionally rename the header when returning it to the client.
|no| -**Example** - -```yaml -default: null -exclude: null -matching: null -named: null -rename: null - -``` - ### Option 1: propagate\.exclude\[\]: array,null @@ -491,13 +453,6 @@ Exclude headers matching these regexes, applied after `matching`. **Items** **Item Type:** `string` -**Example** - -```yaml -{} - -``` - ## Option 2: remove: object @@ -512,15 +467,6 @@ Remove headers matched by the specification. |**matching**||Match headers by regex pattern(s) (OR).
|| |**named**||Match headers by exact name (OR).
|| -**Example** - -```yaml -exclude: null -matching: null -named: null - -``` - ### Option 2: remove\.exclude\[\]: array,null @@ -530,13 +476,6 @@ Exclude headers matching these regexes, applied after `matching`. **Items** **Item Type:** `string` -**Example** - -```yaml -{} - -``` - ## Option 3: insert: object @@ -604,13 +543,6 @@ For more information on the available functions and syntax, see the |**expression**|`string`||yes| -**Example** - -```yaml -algorithm: null - -``` - ### headers\.subgraphs: object,null @@ -643,14 +575,6 @@ and **response** (to clients). Within each list, rules are applied in order. |[**request**](#headerssubgraphsadditionalpropertiesrequest)|`array`|Rules that shape the **request** sent from the router to subgraphs.
|| |[**response**](#headerssubgraphsadditionalpropertiesresponse)|`array`|Rules that shape the **response** sent from the router back to the client.
|| -**Example** - -```yaml -request: null -response: null - -``` - ##### headers\.subgraphs\.additionalProperties\.request\[\]: array,null @@ -683,12 +607,7 @@ to refine the final output. **Example** ```yaml -propagate: - default: null - exclude: null - matching: null - named: null - rename: null +propagate: {} ``` @@ -711,10 +630,7 @@ Useful to drop sensitive or irrelevant headers, or to undo a previous **Example** ```yaml -remove: - exclude: null - matching: null - named: null +remove: {} ``` @@ -743,13 +659,6 @@ insert: {} ``` -**Example** - -```yaml -{} - -``` - ## Option 1: propagate: object @@ -789,17 +698,6 @@ propagate: |**named**||Match headers by exact name (OR).
|| |**rename**|`string`, `null`|Optionally rename the header when forwarding.
|| -**Example** - -```yaml -default: null -exclude: null -matching: null -named: null -rename: null - -``` - ### Option 1: propagate\.exclude\[\]: array,null @@ -809,13 +707,6 @@ Exclude headers matching these regexes, applied after `matching`. **Items** **Item Type:** `string` -**Example** - -```yaml -{} - -``` - ## Option 2: remove: object @@ -830,15 +721,6 @@ Remove headers matched by the specification. |**matching**||Match headers by regex pattern(s) (OR).
|| |**named**||Match headers by exact name (OR).
|| -**Example** - -```yaml -exclude: null -matching: null -named: null - -``` - ### Option 2: remove\.exclude\[\]: array,null @@ -848,13 +730,6 @@ Exclude headers matching these regexes, applied after `matching`. **Items** **Item Type:** `string` -**Example** - -```yaml -{} - -``` - ## Option 3: insert: object @@ -955,12 +830,7 @@ multiple values are returned as separate header fields regardless of `algorithm` **Example** ```yaml -propagate: - default: null - exclude: null - matching: null - named: null - rename: null +propagate: {} ``` @@ -980,10 +850,7 @@ Remove headers before sending the response to the client. **Example** ```yaml -remove: - exclude: null - matching: null - named: null +remove: {} ``` @@ -1005,19 +872,11 @@ For never-join headers, appends another occurrence (multiple lines). **Example** ```yaml -insert: - algorithm: null +insert: {} ``` -**Example** - -```yaml -{} - -``` - ## Option 1: propagate: object @@ -1060,17 +919,6 @@ propagate: |**named**||Match headers by exact name (OR).
|no| |**rename**|`string`, `null`|Optionally rename the header when returning it to the client.
|no| -**Example** - -```yaml -default: null -exclude: null -matching: null -named: null -rename: null - -``` - ### Option 1: propagate\.exclude\[\]: array,null @@ -1080,13 +928,6 @@ Exclude headers matching these regexes, applied after `matching`. **Items** **Item Type:** `string` -**Example** - -```yaml -{} - -``` - ## Option 2: remove: object @@ -1101,15 +942,6 @@ Remove headers matched by the specification. |**matching**||Match headers by regex pattern(s) (OR).
|| |**named**||Match headers by exact name (OR).
|| -**Example** - -```yaml -exclude: null -matching: null -named: null - -``` - ### Option 2: remove\.exclude\[\]: array,null @@ -1119,13 +951,6 @@ Exclude headers matching these regexes, applied after `matching`. **Items** **Item Type:** `string` -**Example** - -```yaml -{} - -``` - ## Option 3: insert: object @@ -1193,13 +1018,6 @@ For more information on the available functions and syntax, see the |**expression**|`string`||yes| -**Example** - -```yaml -algorithm: null - -``` - ## http: object diff --git a/lib/router-config/src/headers.rs b/lib/router-config/src/headers.rs index 676e157a9..bd0913f6f 100644 --- a/lib/router-config/src/headers.rs +++ b/lib/router-config/src/headers.rs @@ -41,29 +41,8 @@ pub static NEVER_JOIN_HEADERS: &[&str] = &["set-cookie", "www-authenticate"]; /// ## Safety /// Hop-by-hop headers are always stripped. Never-join headers (e.g. `set-cookie`) /// are never comma-joined. Multiple values are preserved as separate fields. -/// -/// ### Example -/// ```yaml -/// headers: -/// all: -/// request: -/// - propagate: -/// named: Authorization -/// - remove: -/// matching: "^x-legacy-.*" -/// - insert: -/// name: x-router -/// value: hive-router -/// -/// subgraphs: -/// accounts: -/// request: -/// - propagate: -/// named: x-tenant-id -/// rename: x-acct-tenant -/// default: unknown -/// ``` #[derive(Debug, Default, Deserialize, Serialize, JsonSchema, Clone)] +#[schemars(example = headers_example_1())] pub struct HeadersConfig { /// Rules applied to all subgraphs (global defaults). #[serde(default, skip_serializing_if = "Option::is_none")] @@ -78,6 +57,49 @@ pub struct HeadersConfig { pub subgraphs: Option>, } +fn headers_example_1() -> HeadersConfig { + HeadersConfig { + all: Some(HeaderRules { + request: Some(vec![ + RequestHeaderRule::Propagate(RequestPropagateRule { + spec: MatchSpec { + named: Some(OneOrMany::One("Authorization".to_string())), + ..Default::default() + }, + ..Default::default() + }), + RequestHeaderRule::Remove(RemoveRule { + spec: MatchSpec { + matching: Some(OneOrMany::One("^x-legacy-.*".to_string())), + ..Default::default() + }, + }), + RequestHeaderRule::Insert(RequestInsertRule { + name: "x-router".to_string(), + source: InsertSource::Value { + value: "hive-router".to_string(), + }, + }), + ]), + response: None, + }), + subgraphs: Some(HashMap::from([( + "accounts".to_string(), + HeaderRules { + request: Some(vec![RequestHeaderRule::Propagate(RequestPropagateRule { + spec: MatchSpec { + named: Some(OneOrMany::One("x-tenant-id".to_string())), + ..Default::default() + }, + rename: Some("x-acct-tenant".to_string()), + default: Some("unknown".to_string()), + })]), + response: None, + }, + )])), + } +} + /// Rules for a single scope (global or per-subgraph). /// /// You can specify independent rule lists for **request** (to subgraphs) @@ -85,11 +107,11 @@ pub struct HeadersConfig { #[derive(Debug, Deserialize, Serialize, JsonSchema, Clone, Default)] pub struct HeaderRules { /// Rules that shape the **request** sent from the router to subgraphs. - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub request: Option>, /// Rules that shape the **response** sent from the router back to the client. - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub response: Option>, } @@ -199,7 +221,7 @@ pub struct ResponseInsertRule { pub source: InsertSource, /// How to merge values across multiple subgraph responses. /// Default: `Last` (overwrite). - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub algorithm: Option, } @@ -268,15 +290,15 @@ pub enum OneOrMany { #[derive(Debug, Deserialize, Serialize, JsonSchema, Clone, Default)] pub struct MatchSpec { /// Match headers by exact name (OR). - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub named: Option>, /// Match headers by regex pattern(s) (OR). - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub matching: Option>, /// Exclude headers matching these regexes, applied after `matching`. - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub exclude: Option>, } @@ -304,19 +326,18 @@ pub struct MatchSpec { /// named: Authorization /// default: "Bearer test-token" /// ``` -#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)] +#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone, Default)] pub struct RequestPropagateRule { #[serde(flatten)] pub spec: MatchSpec, /// Optionally rename the header when forwarding. - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub rename: Option, /// If the header is missing, set a default value. /// Applied only when **none** of the matched headers exist. - - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub default: Option, } @@ -372,11 +393,11 @@ pub struct ResponsePropagateRule { pub spec: MatchSpec, /// Optionally rename the header when returning it to the client. - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub rename: Option, /// If no subgraph returns the header, set this default value. - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub default: Option, /// How to merge values across multiple subgraph responses.