Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
499d5c9
Add pre-dispatch hooks and explicit request ingress fidelity
prk-Jr Oct 5, 2026
75067d2
Clarify ingress preservation per request
prk-Jr Oct 6, 2026
4f221d2
Clarify authority validation and fidelity evidence
prk-Jr Oct 6, 2026
965d482
Merge main into spec/pre-dispatch-request-fidelity
prk-Jr Oct 8, 2026
08c00e8
Fix Cloudflare primary URI conversion compatibility
prk-Jr Oct 9, 2026
78439fd
Merge main and reconcile Fastly ingress snapshot APIs
prk-Jr Oct 9, 2026
02ab00d
Address request ingress fidelity review findings
prk-Jr Oct 15, 2026
c0a90d6
Merge branch 'main' into spec/pre-dispatch-request-fidelity
prk-Jr Oct 16, 2026
70876c9
Merge remote-tracking branch 'origin/main' into spec/pre-dispatch-req…
prk-Jr Oct 19, 2026
d6e3f4c
fix: address request ingress fidelity review findings
prk-Jr Oct 19, 2026
52121d9
fix: address approved request fidelity review nits
prk-Jr Oct 19, 2026
295fab8
Merge branch 'main' into spec/pre-dispatch-request-fidelity
aram356 Oct 22, 2026
ca25213
fix(ci): verify cache seeds and retry throttled writes
prk-Jr Oct 22, 2026
251a472
Move cache seed hardening into its own change
prk-Jr Oct 22, 2026
7b6f913
Resolve request fidelity review documentation and tooling gaps
prk-Jr Oct 22, 2026
83b7fae
fix: close request fidelity review gaps
prk-Jr Oct 23, 2026
d05d552
Merge branch 'main' into spec/pre-dispatch-request-fidelity
prk-Jr Oct 23, 2026
3475002
Merge branch 'main' into spec/pre-dispatch-request-fidelity
prk-Jr Oct 24, 2026
f6d0ed6
fix: tighten pre-dispatch contract and review regressions
prk-Jr Oct 26, 2026
1d1ad91
Merge branch 'main' into spec/pre-dispatch-request-fidelity
aram356 Oct 27, 2026
0f7ff3b
fix: address pin guard and hook documentation review
prk-Jr Oct 27, 2026
9620d6c
Merge branch 'main' into spec/pre-dispatch-request-fidelity
aram356 Oct 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 116 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ jobs:
~/.cargo/git/db/
target/
tests/fixtures/reusable-app/target/
key: ${{ runner.os }}-cargo-${{ matrix.suite }}-tests-${{ hashFiles('**/Cargo.lock') }}
tests/fixtures/request-fidelity-runner/target/
key: ${{ runner.os }}-cargo-${{ matrix.suite }}-tests-ingress-v1-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-${{ matrix.suite }}-tests-ingress-v1-
${{ runner.os }}-cargo-${{ matrix.suite }}-tests-

- name: Retrieve Rust version
Expand All @@ -49,6 +51,7 @@ jobs:
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: ${{ steps.rust-version.outputs.rust-version }}
components: clippy, rustfmt

- name: Install wasm targets for generated-project checks
if: matrix.suite == 'cli'
Expand All @@ -70,7 +73,10 @@ jobs:
# deploy fixture). A mismatch only surfaces as a confusing
# "trait bound not satisfied" in a deploy smoke job, so gate it here.
- name: Shared dependency pins agree
run: ./scripts/check_shared_dep_pins.sh
if: matrix.suite == 'workspace'
run: |
./scripts/check_shared_dep_pins.sh
Comment thread
prk-Jr marked this conversation as resolved.
bash scripts/test_shared_dep_pins.sh

- name: Nested AppConfig audit
if: matrix.suite == 'cli'
Expand All @@ -80,6 +86,13 @@ jobs:
if: matrix.suite == 'cli'
run: cargo test -p edgezero-cli --features nested-app-config-check --bin check_no_nested_app_config

- name: Verify native request ingress harness
if: matrix.suite == 'workspace'
run: |
cargo fmt --manifest-path tests/fixtures/request-fidelity-runner/Cargo.toml --all -- --check
cargo test --manifest-path tests/fixtures/request-fidelity-runner/Cargo.toml --locked
cargo clippy --manifest-path tests/fixtures/request-fidelity-runner/Cargo.toml --all-targets --locked -- -D warnings

- name: Lifecycle fixture host tests
if: matrix.suite == 'workspace'
run: >-
Expand Down Expand Up @@ -178,8 +191,10 @@ jobs:
~/.wasmtime/bin/
target/
tests/fixtures/reusable-app/target/
key: ${{ runner.os }}-cargo-${{ matrix.adapter }}-${{ hashFiles('**/Cargo.lock', '.tool-versions') }}
tests/fixtures/request-fidelity/target/
key: ${{ runner.os }}-cargo-${{ matrix.adapter }}-ingress-v1-${{ hashFiles('**/Cargo.lock', '.tool-versions') }}
restore-keys: |
${{ runner.os }}-cargo-${{ matrix.adapter }}-ingress-v1-
${{ runner.os }}-cargo-${{ matrix.adapter }}-

- name: Retrieve Rust version
Expand All @@ -191,10 +206,35 @@ jobs:
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: ${{ steps.rust-version.outputs.rust-version }}
components: clippy, rustfmt

- name: Add wasm target
run: rustup target add ${{ matrix.target }}

- name: Set up matching browser and driver
if: matrix.adapter == 'cloudflare'
id: chrome
uses: browser-actions/setup-chrome@v2
with:
chrome-version: 154.0.8037.92
install-chromedriver: true
install-dependencies: true

- name: Set up Node for local Worker tooling
if: matrix.adapter == 'cloudflare'
uses: actions/setup-node@v6
with:
node-version-file: .tool-versions
cache: npm
cache-dependency-path: tests/fixtures/request-fidelity/package-lock.json

- name: Bind browser tests to pinned Chrome
if: matrix.adapter == 'cloudflare'
env:
CHROME_BINARY: ${{ steps.chrome.outputs.chrome-path }}
run: |
node -e 'require("node:fs").writeFileSync(require("node:path").join(process.env.RUNNER_TEMP, "edgezero-webdriver.json"), JSON.stringify({"goog:chromeOptions": {binary: process.env.CHROME_BINARY}}))'

- name: Resolve wasm-bindgen CLI version
if: matrix.adapter == 'cloudflare'
id: wasm-bindgen-version
Expand Down Expand Up @@ -285,6 +325,8 @@ jobs:

- name: Run ${{ matrix.adapter }} wasm tests
env:
CHROMEDRIVER: ${{ steps.chrome.outputs.chromedriver-path }}
WASM_BINDGEN_TEST_WEBDRIVER_JSON: ${{ runner.temp }}/edgezero-webdriver.json
${{ matrix.runner_env }}: ${{ matrix.runner_value }}
run: cargo test -p edgezero-adapter-${{ matrix.adapter }} --features ${{ matrix.adapter }} --target ${{ matrix.target }} --test contract

Expand All @@ -298,6 +340,77 @@ jobs:
${{ matrix.runner_env }}: ${{ matrix.runner_value }}
run: cargo test -p edgezero-adapter-fastly --features fastly --target wasm32-wasip1 --lib

- name: Read fixture worker-build pin
if: matrix.adapter == 'cloudflare'
id: worker-build-pin
shell: bash
run: |
version="$(awk '{ sub(/[[:space:]]*#.*/, "") } /^\[/ { in_pins=($0 == "[package.metadata.request-fidelity]") } in_pins && $1 == "worker-build" && $2 == "=" { gsub(/"/, "", $3); count++; found=$3 } END { if (count == 1) print found }' tests/fixtures/request-fidelity/Cargo.toml)"
test -n "$version"
echo "version=$version" >> "$GITHUB_OUTPUT"

# The pinned release has no Linux prebuilt. Keep its compiled executable
# separate from Cargo's broad bin cache and verify it on every restore.
- name: Cache pinned worker-build executable
if: matrix.adapter == 'cloudflare'
uses: actions/cache@v5
with:
path: ${{ runner.temp }}/worker-build
key: worker-build-${{ runner.os }}-${{ runner.arch }}-${{ steps.worker-build-pin.outputs.version }}

- name: Install lock-matched worker-build
if: matrix.adapter == 'cloudflare'
shell: bash
env:
VERSION: ${{ steps.worker-build-pin.outputs.version }}
run: |
install_dir="$RUNNER_TEMP/worker-build"
if [[ "$("$install_dir/bin/worker-build" --version 2>/dev/null || true)" != "$VERSION" ]]; then
cargo install worker-build --version "$VERSION" --locked --root "$install_dir" --force
fi
test "$("$install_dir/bin/worker-build" --version)" = "$VERSION"
echo "$install_dir/bin" >> "$GITHUB_PATH"

- name: Install pinned Spin HTTP runtime
if: matrix.adapter == 'spin'
shell: bash
run: |
version="$(awk '{ sub(/[[:space:]]*#.*/, "") } /^\[/ { in_pins=($0 == "[package.metadata.request-fidelity]") } in_pins && $1 == "spin-cli" && $2 == "=" { gsub(/"/, "", $3); count++; found=$3 } END { if (count == 1) print found }' tests/fixtures/request-fidelity/Cargo.toml)"
test "$version" = "4.1.0"
install_dir="$RUNNER_TEMP/spin"
mkdir -p "$install_dir"
curl --retry 3 -fL "https://github.com/spinframework/spin/releases/download/v${version}/spin-v${version}-linux-amd64.tar.gz" -o "$RUNNER_TEMP/spin.tar.gz"
# GitHub's published digest for the v4.1.0 linux-amd64 release asset.
# Keep the version and digest paired when updating the fixture pin.
echo "aeeb892f94cf861f5302546faa0bdaa82cc4be30539621299746a6dc011f3b5a $RUNNER_TEMP/spin.tar.gz" | sha256sum --check --strict
tar -xzf "$RUNNER_TEMP/spin.tar.gz" -C "$install_dir"
echo "$install_dir" >> "$GITHUB_PATH"
"$install_dir/spin" --version

- name: Check isolated fixture formatting
run: cargo fmt --manifest-path tests/fixtures/request-fidelity/Cargo.toml --all -- --check

- name: Lint isolated ingress fixture
run: >-
cargo clippy --manifest-path tests/fixtures/request-fidelity/Cargo.toml
--features ${{ matrix.adapter }} --target ${{ matrix.target }}
--all-targets --locked -- -D warnings

- name: Run local HTTP ingress probes
shell: bash
run: |
if [ "${{ matrix.adapter }}" = "cloudflare" ]; then
version="$(awk '$1 == "name" && $3 == "\"wasm-bindgen\"" { in_pkg=1; next } in_pkg && $1 == "version" { gsub(/"/, "", $3); print $3; exit }' tests/fixtures/request-fidelity/Cargo.lock)"
test -n "$version"
WASM_BINDGEN_BIN="$(command -v wasm-bindgen || true)"
if [[ -z "$WASM_BINDGEN_BIN" ]] || [[ "$("$WASM_BINDGEN_BIN" --version 2>/dev/null || true)" != "wasm-bindgen $version" ]]; then
cargo install wasm-bindgen-cli --version "$version" --locked --root "$RUNNER_TEMP/fixture-wasm-bindgen"
WASM_BINDGEN_BIN="$RUNNER_TEMP/fixture-wasm-bindgen/bin/wasm-bindgen"
fi
export WASM_BINDGEN_BIN
fi
./scripts/test-request-fidelity.sh ${{ matrix.adapter }}

- name: Fastly reusable HTTP smoke
if: matrix.adapter == 'fastly'
run: ./scripts/smoke_test_reusable_app.sh --adapter fastly --suite smoke
Expand Down
167 changes: 163 additions & 4 deletions crates/edgezero-adapter-axum/src/dev_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ use std::path::{Path, PathBuf};
use std::str::FromStr as _;
use std::sync::Arc;

use crate::request::HttpTransport;
use anyhow::Context as _;
use axum::Router;
use axum::body::Body as AxumBody;
use axum::http::Request as AxumRequest;
use tokio::net::TcpListener as TokioTcpListener;
use tokio::runtime::Builder as RuntimeBuilder;
use tokio::signal;
Expand Down Expand Up @@ -299,10 +302,12 @@ async fn serve_with_stores(
}
service
};
let axum_router = Router::new().fallback_service(service_fn(move |req| {
let mut svc = service.clone();
async move { svc.call(req).await }
}));
let axum_router =
Router::new().fallback_service(service_fn(move |mut req: AxumRequest<AxumBody>| {
req.extensions_mut().insert(HttpTransport);
let mut svc = service.clone();
async move { svc.call(req).await }
}));
let make_service = axum_router.into_make_service_with_connect_info::<SocketAddr>();

let shutdown = enable_ctrl_c.then_some(async {
Expand Down Expand Up @@ -748,6 +753,23 @@ mod integration_tests {
use tokio::task::{JoinHandle, spawn_blocking};
use tokio::time::sleep;

use async_trait::async_trait;
use edgezero_core::body::Body;
use edgezero_core::http::{HeaderValue, Request, Response, response_builder};
use edgezero_core::ingress::{CapturedTarget, Preservation, RequestIngress};
use edgezero_core::middleware::{Middleware, Next};
use edgezero_core::router::PreDispatchHook;
use std::io::{Read as _, Write as _};
use std::net::TcpStream;
use std::sync::atomic::{AtomicUsize, Ordering};

struct IngressHook {
calls: Arc<AtomicUsize>,
}
struct IngressMiddleware {
calls: Arc<AtomicUsize>,
}

struct TestServer {
_temp_dir: tempfile::TempDir,
base_url: String,
Expand All @@ -759,6 +781,143 @@ mod integration_tests {
handle: JoinHandle<()>,
}

#[async_trait(?Send)]
impl Middleware for IngressMiddleware {
async fn handle(&self, ctx: RequestContext, next: Next<'_>) -> Result<Response, EdgeError> {
self.calls.fetch_add(1, Ordering::SeqCst);
next.run(ctx).await
}
}

#[async_trait(?Send)]
impl PreDispatchHook for IngressHook {
async fn handle(&self, request: &mut Request) -> Result<Option<Response>, EdgeError> {
self.calls.fetch_add(1, Ordering::SeqCst);
let ingress = request
.extensions()
.get::<RequestIngress>()
.expect("should carry adapter metadata");
assert_eq!(
ingress
.origin()
.expect("should expose HTTP binding")
.scheme(),
"http"
);
let CapturedTarget::Complete(target) = ingress.target() else {
panic!("should capture target")
};
assert_eq!(target.value(), request.uri().to_string());
match request.headers()["x-fixture"]
.to_str()
.expect("should read fixed fixture")
{
"repeat" => {
assert_eq!(request.method().as_str(), "EXAMPLE-METHOD");
assert_eq!(request.headers().get_all("origin").iter().count(), 2);
assert_eq!(request.headers().get_all("x-control").iter().count(), 2);
let cookies: Vec<_> = request
.headers()
.get_all("cookie")
.iter()
.map(HeaderValue::as_bytes)
.collect();
assert_eq!(cookies, [b"a=\xff".as_slice(), b"b=2".as_slice()]);
}
"length" => {
assert_eq!(
request.headers().get_all("content-length").iter().count(),
1
);
assert_eq!(
ingress
.header_fidelity(&"content-length".parse().expect("should parse name"))
.field_multiplicity(),
Preservation::Unknown
);
}
"te-cl" => assert!(!request.headers().contains_key("content-length")),
"continue" => {
return Ok(None);
}
"error" => return Err(EdgeError::bad_request("fixture rejection")),
"dot" => assert_eq!(request.uri().path(), "/reserved/%2e/../%2F//"),
_ => panic!("should use fixed fixture"),
}
Ok(Some(
response_builder()
.status(204)
.header("x-hook", "yes")
.body(Body::empty())
.map_err(EdgeError::internal)?,
))
}
}

#[action]
async fn ingress_ordinary() -> &'static str {
"continued"
}

fn raw_ingress_response(addr: SocketAddr, fixture: &[u8]) -> Vec<u8> {
let mut connection = TcpStream::connect(addr).expect("should connect listener");
connection
.set_read_timeout(Some(Duration::from_secs(3)))
.expect("should set timeout");
connection
.write_all(fixture)
.expect("should send exact request");
let mut response = Vec::new();
connection
.read_to_end(&mut response)
.expect("should read response");
response
}

#[tokio::test(flavor = "multi_thread")]
async fn ingress_raw_tcp_distinguishes_parser_and_hook_behaviour() {
let hook_calls = Arc::new(AtomicUsize::new(0));
let middleware_calls = Arc::new(AtomicUsize::new(0));
let router = RouterService::builder()
.middleware(IngressMiddleware {
calls: Arc::clone(&middleware_calls),
})
.pre_dispatch_hook_arc(Arc::new(IngressHook {
calls: Arc::clone(&hook_calls),
}))
.get("/ordinary", ingress_ordinary)
.build();
let server = start_test_server(router).await;
let addr: SocketAddr = server
.base_url
.strip_prefix("http://")
.expect("should have HTTP binding")
.parse()
.expect("should parse listener address");
let cases: [(&[u8], &[u8], usize); 7] = [
(b"EXAMPLE-METHOD /missing HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\nX-Fixture: repeat\r\nCookie: a=\xff\r\nOrigin: https://example.com\r\nX-Control: first\r\nCOOKIE: b=2\r\nOrigin: https://other.example.com\r\nX-Control: second\r\n\r\n", b"HTTP/1.1 204", 1),
(b"POST /missing HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\nX-Fixture: length\r\nContent-Length: 0\r\nContent-Length: 0\r\n\r\n", b"HTTP/1.1 204", 1),
(b"POST /missing HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\nX-Fixture: length\r\nContent-Length: 0\r\nContent-Length: 1\r\n\r\n", b"HTTP/1.1 400", 0),
(b"GET /reserved/%2e/../%2F// HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\nX-Fixture: dot\r\n\r\n", b"HTTP/1.1 204", 1),
(b"GET /ordinary HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\nX-Fixture: continue\r\n\r\n", b"HTTP/1.1 200", 1),
(b"GET /missing HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\nX-Fixture: error\r\n\r\n", b"HTTP/1.1 400", 1),
(b"POST /missing HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\nX-Fixture: te-cl\r\nTransfer-Encoding: chunked\r\nContent-Length: 0\r\n\r\n0\r\n\r\n", b"HTTP/1.1 204", 1),
];
for (fixture, status, calls) in cases {
let before = hook_calls.load(Ordering::SeqCst);
let response = spawn_blocking(move || raw_ingress_response(addr, fixture))
.await
.expect("should join socket probe");
assert!(
response.starts_with(status),
"should return expected fixed fixture status"
);
assert_eq!(hook_calls.load(Ordering::SeqCst) - before, calls);
}
assert_eq!(middleware_calls.load(Ordering::SeqCst), 1);
server.handle.abort();
}

async fn start_test_server(router: RouterService) -> TestServer {
let listener = TokioTcpListener::bind("127.0.0.1:0")
.await
Expand Down
Loading
Loading